PROJECT GUIDE · 08

Implement Infrastructure as Code with Terraform & Ansible

Transform all manual infrastructure setup from Projects 1–6 into code. Use Terraform to define and provision infrastructure declaratively, and Ansible to automate server configuration. Your entire ABC Retail environment will be reproducible with a single command.

Environment
Ubuntu VM in VirtualBox
Terraform + Ansible locally
Difficulty
⭐⭐⭐⭐ Advanced Beginner
First IaC project
Course Module
Chapter 8
DevOps Practices & IaC
Duration
5–7 Days
Terraform + Ansible + Git

1. IaC Workflow Architecture

Infrastructure as Code (IaC) treats server configuration and cloud resource provisioning the same way software developers treat application code — it is version controlled, peer reviewed, tested, and automated. The key benefit: any environment (dev, staging, production) can be created identically and reproducibly. Manual configurations that exist only in someone's memory are eliminated.

IaC AUTOMATION PIPELINE — TERRAFORM + ANSIBLE 📝 WRITE .tf CODE Resources declared 🔍 TERRAFORM PLAN Preview changes 👁 PEER REVIEW Git PR approval 🚀 TERRAFORM APPLY Infra provisioned ANSIBLE PLAY Software configured ENVIRONMENT READY 100% automated 🏗 TERRAFORM — Infrastructure Provisioning Creates and manages cloud resources (VMs, Networks, DBs) Declarative language (HCL) — define WHAT you want, not HOW Tracks state in terraform.tfstate — knows what already exists terraform init → plan → apply → destroy ⚙ ANSIBLE — Configuration Management Configures software on provisioned servers (no agent needed) Playbooks (YAML) — tasks run in order over SSH Idempotent — safe to run multiple times, same result ansible-playbook -i inventory setup.yml

2. Step-by-Step Action Items

PHASE 1 · STEP 1 Install Terraform and Set Up Git Repository
1
On your Ubuntu VM, install Terraform from HashiCorp's official repository:
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list sudo apt update && sudo apt install terraform -y # Verify installation terraform version
We install from the official HashiCorp repository to ensure we get the latest stable version with security patches. The terraform version command shows the installed version. Terraform uses HCL (HashiCorp Configuration Language) — a human-friendly format for defining infrastructure. All Terraform files end with .tf extension.
2
Create the IaC project directory with Git version control:
mkdir -p ~/abc-retail-iac/{terraform,ansible,scripts} cd ~/abc-retail-iac # Initialize Git repository git init git config user.email "cloudadmin@abc-retail.com" git config user.name "Cloud Admin" # Create .gitignore (IMPORTANT: never commit state files or secrets) cat > .gitignore << 'EOF' # Terraform state files (contain sensitive info) *.tfstate *.tfstate.backup .terraform/ *.tfvars # Ansible *.retry vault_password # Secrets .env *.pem *.key EOF git add .gitignore git commit -m "Initial IaC repository setup"
Version controlling IaC code is mandatory — changes to infrastructure code should be tracked, reviewed, and auditable just like application code. The .gitignore file prevents sensitive files from being committed. The terraform.tfstate file contains the current state of all managed infrastructure including resource IDs, IP addresses, and sometimes credentials — committing it to a repository would be a serious security incident. The *.tfvars files contain variable values (often including passwords) — these should also never be committed, even to private repositories.
PHASE 1 · STEP 2 Write Terraform Code to Manage Docker Infrastructure

Since this course is done locally (no cloud account required), we use the Terraform Docker Provider to manage our Docker containers as Infrastructure as Code. The same Terraform concepts and workflow apply identically when using the AWS, Azure, or GCP providers — only the resource types change.

1
Create the main Terraform configuration file:
nano ~/abc-retail-iac/terraform/main.tf
2
Type the complete Terraform configuration:
# ============================================================ # main.tf — ABC Retail Infrastructure as Code # Provider: Docker (local simulation of cloud provider) # To apply: cd terraform && terraform init && terraform apply # ============================================================ # ---- PROVIDER CONFIGURATION ---- # This section declares which cloud/tool provider to use terraform { required_providers { docker = { source = "kreuzwerker/docker" version = "~> 3.0" } } # In production: store state remotely (S3, Azure Blob, GCS) # backend "s3" { # bucket = "abc-retail-terraform-state" # key = "prod/terraform.tfstate" # region = "ap-south-1" # } } provider "docker" { host = "unix:///var/run/docker.sock" } # ---- VARIABLES ---- variable "web_port" { description = "Port on which the web application is exposed" type = number default = 8080 } variable "web_replicas" { description = "Number of web server containers to run" type = number default = 2 } variable "app_version" { description = "Docker image version tag to deploy" type = string default = "v1.0" } # ---- DOCKER NETWORK ---- resource "docker_network" "abc_retail_net" { name = "abc-retail-terraform-net" driver = "bridge" labels { label = "project" value = "abc-retail" } labels { label = "managed-by" value = "terraform" } } # ---- DOCKER VOLUMES ---- resource "docker_volume" "web_logs" { name = "terraform-web-logs" driver = "local" } resource "docker_volume" "db_data" { name = "terraform-db-data" driver = "local" } # ---- PULL DOCKER IMAGE ---- resource "docker_image" "abc_retail" { name = "abc-retail:${var.app_version}" keep_locally = true # Don't delete image when resource is destroyed } resource "docker_image" "mariadb" { name = "mariadb:10.11" keep_locally = true } # ---- WEB SERVER CONTAINERS ---- resource "docker_container" "web" { count = var.web_replicas # Creates N containers based on variable name = "abc-retail-web-${count.index + 1}" image = docker_image.abc_retail.image_id restart = "unless-stopped" ports { internal = 80 external = var.web_port + count.index # 8080, 8081, etc. } volumes { volume_name = docker_volume.web_logs.name container_path = "/var/log/nginx" } networks_advanced { name = docker_network.abc_retail_net.name } labels { label = "managed-by" value = "terraform" } labels { label = "environment" value = "production" } healthcheck { test = ["CMD", "curl", "-f", "http://localhost/"] interval = "30s" timeout = "5s" retries = 3 } } # ---- DATABASE CONTAINER ---- resource "docker_container" "database" { name = "abc-retail-db-terraform" image = docker_image.mariadb.image_id restart = "unless-stopped" env = [ "MYSQL_ROOT_PASSWORD=SecureRoot@TF2024", "MYSQL_DATABASE=abc_retail", "MYSQL_USER=retailapp", "MYSQL_PASSWORD=AppDB@TF2024" ] volumes { volume_name = docker_volume.db_data.name container_path = "/var/lib/mysql" } networks_advanced { name = docker_network.abc_retail_net.name } labels { label = "managed-by" value = "terraform" } } # ---- OUTPUTS ---- output "web_container_names" { description = "Names of deployed web containers" value = [for c in docker_container.web : c.name] } output "web_urls" { description = "URLs to access each web server" value = [for i in range(var.web_replicas) : "http://localhost:${var.web_port + i}"] } output "database_container" { description = "Database container name" value = docker_container.database.name } output "network_name" { description = "Docker network for inter-container communication" value = docker_network.abc_retail_net.name }
This Terraform configuration demonstrates all core concepts: Provider — tells Terraform which API to use (here: Docker). Resources — the infrastructure components to create (docker_network, docker_volume, docker_container). Variables — parameterize the configuration so values can be changed without modifying the code. count — creates multiple identical resources (here: N web containers). Outputs — display useful information after applying. The count.index trick automatically names and ports web containers sequentially. In production, this same structure would use aws_instance, aws_vpc, aws_security_group resources instead of Docker resources.
3
Initialize Terraform (downloads the Docker provider plugin):
cd ~/abc-retail-iac/terraform terraform init
The terraform init command reads the provider requirements from main.tf and downloads the required provider plugins. You should see "Terraform has been successfully initialized!" A .terraform directory is created containing the downloaded provider binaries. This is analogous to npm install for JavaScript projects or pip install for Python — it sets up the local workspace with required dependencies.
4
Preview what Terraform will create:
terraform plan
The terraform plan command shows exactly what Terraform will do when applied — a "diff" of infrastructure changes. Green lines with + are resources that will be CREATED. Yellow lines with ~ are resources that will be MODIFIED. Red lines with - are resources that will be DESTROYED. This preview step is critical in production — always review the plan before applying to avoid unintended changes. The output shows exactly how many resources will be added, changed, and destroyed.
5
Apply the configuration to create all infrastructure:
terraform apply # When prompted: "Do you want to perform these actions?" # Type: yes
The terraform apply command executes the plan and creates all defined resources. After confirmation, watch as Terraform creates the network, volumes, images, and containers in the correct order (respecting dependencies — the network must exist before containers can join it). When complete, the output values are displayed. Run docker ps to verify the containers were created by Terraform — you should see the abc-retail-web-1, abc-retail-web-2, and database containers.
6
View the Terraform state to understand what it tracks:
terraform state list terraform show
The terraform state list command shows all resources managed by Terraform. The terraform show command displays the full details of every resource in the state file. The state file is Terraform's database — it records the real-world IDs of every resource it manages so that on the next apply, it can calculate what changed. If the state file is lost, Terraform loses track of what it manages and may try to create duplicate resources.
7
Test infrastructure changes — change the number of web replicas:
# Scale from 2 to 3 web containers with ONE command terraform apply -var="web_replicas=3"
This demonstrates the power of IaC — scaling infrastructure is a one-command operation. Terraform calculates the difference: currently 2 containers exist, 3 are desired, so 1 new container will be created. No manual steps required. In cloud environments, this same command would add a new EC2 instance to an Auto Scaling Group, update load balancer target groups, and configure all networking — all automated and tracked in state. Compare this to the manual approach: SSH to a server, install software, configure it, update the load balancer configuration, test...
PHASE 1 · STEP 3 Separate Configuration with Variable Files
1
Create separate variable files for different environments:
# Development environment variables cat > ~/abc-retail-iac/terraform/dev.tfvars << 'EOF' web_port = 8080 web_replicas = 1 app_version = "v1.0" EOF # Production environment variables cat > ~/abc-retail-iac/terraform/prod.tfvars << 'EOF' web_port = 80 web_replicas = 3 app_version = "v1.0" EOF
Separating configuration from code is a key DevOps principle. The same Terraform code deploys to different environments with different parameters: dev has 1 replica (cost saving), prod has 3 (high availability). The code itself doesn't change — only the variable values. In cloud environments, you'd have separate prod.tfvars with production-scale values (many replicas, larger instance types, multi-region) and dev.tfvars with minimal resources. To deploy to a specific environment: terraform apply -var-file="prod.tfvars".
2
Destroy the infrastructure (to reset for Ansible demo):
terraform destroy
The terraform destroy command removes ALL resources managed by this Terraform configuration — the reverse of apply. It shows a plan of what will be destroyed and asks for confirmation. This is the "tear down" operation — extremely useful for development environments that should be cleaned up at end of day. In production, terraform destroy would be protected by IAM policies, approval workflows, and backup requirements.
PHASE 2 · STEP 1 Install Ansible and Write Configuration Playbooks

While Terraform provisions infrastructure (creates servers, networks), Ansible configures the software on those servers (installs Nginx, configures firewall rules, creates users). Together they form a complete IaC solution. Ansible uses SSH to connect to servers and applies configuration steps described in YAML files called "playbooks."

1
Install Ansible:
sudo apt install ansible -y ansible --version
Ansible is "agentless" — it doesn't require any software installed on the managed servers. It only needs SSH access and Python (pre-installed on Ubuntu). This makes it much simpler to adopt than alternatives like Chef or Puppet, which require installing an agent on every managed server. Ansible connects via SSH, pushes Python scripts, executes them, and returns the result. After execution, nothing is left running on the target server.
2
Create the Ansible inventory file (list of servers to manage):
cat > ~/abc-retail-iac/ansible/inventory.ini << 'EOF' # Ansible Inventory — List of managed servers # Group: [groupname] # Server: hostname/IP followed by connection settings [webservers] abc-web-01 ansible_host=192.168.56.101 ansible_user=cloudadmin ansible_ssh_private_key_file=~/.ssh/abc-retail-key [databases] abc-db-01 ansible_host=192.168.56.102 ansible_user=cloudadmin ansible_ssh_private_key_file=~/.ssh/abc-retail-key [abc_retail:children] webservers databases [abc_retail:vars] ansible_python_interpreter=/usr/bin/python3 EOF
The inventory file defines which servers Ansible manages and how to connect to them. The [webservers] and [databases] groups allow you to target different sets of servers with different playbooks. ansible_host is the IP address, ansible_user is the SSH username, and ansible_ssh_private_key_file specifies the SSH key from Project 6. The [abc_retail:children] section creates a parent group containing both webservers and databases. In dynamic cloud environments, Ansible can automatically generate inventories from cloud APIs instead of static files.
3
Test connectivity to all managed servers:
cd ~/abc-retail-iac/ansible ansible all -i inventory.ini -m ping
The ansible all -m ping command runs the ping module on all servers in the inventory. This is not an ICMP ping — it's an Ansible connectivity test that: connects via SSH, verifies Python is available, and returns "pong" if everything works. If you see "pong" for all servers, Ansible can manage them. If you see connection errors, check the IP addresses in inventory and that the SSH key is correct.
4
Write the main configuration playbook:
nano ~/abc-retail-iac/ansible/setup-webserver.yml
5
Type the complete playbook:
--- # ============================================================ # Ansible Playbook: setup-webserver.yml # Purpose: Configure ABC Retail web server from scratch # Run: ansible-playbook -i inventory.ini setup-webserver.yml # ============================================================ - name: "Configure ABC Retail Web Server" hosts: webservers # Apply to all servers in [webservers] group become: yes # Use sudo for all tasks vars: docker_compose_version: "2.21.0" web_port: 8080 tasks: # ---- System Updates ---- - name: "Update apt package cache" apt: update_cache: yes cache_valid_time: 3600 # Don't update if cache is less than 1 hour old - name: "Install required system packages" apt: name: - curl - wget - git - ufw - ca-certificates - gnupg state: present # Ensure these packages are installed # ---- Security Hardening ---- - name: "Configure UFW - allow SSH" ufw: rule: allow port: "22" proto: tcp comment: "SSH Access" - name: "Configure UFW - allow web traffic" ufw: rule: allow port: "{{ web_port }}" proto: tcp comment: "ABC Retail Web Application" - name: "Enable UFW firewall" ufw: state: enabled policy: deny # ---- Docker Installation ---- - name: "Add Docker GPG key" apt_key: url: https://download.docker.com/linux/ubuntu/gpg state: present - name: "Add Docker repository" apt_repository: repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_lsb.codename }} stable" state: present - name: "Install Docker Engine" apt: name: - docker-ce - docker-ce-cli - containerd.io - docker-compose-plugin state: present - name: "Start and enable Docker service" systemd: name: docker state: started enabled: yes - name: "Add cloudadmin user to docker group" user: name: cloudadmin groups: docker append: yes # ---- Deploy Application ---- - name: "Create application directory" file: path: /opt/abc-retail state: directory mode: "0755" - name: "Copy docker-compose.yml to server" copy: src: ../terraform/../ansible/files/docker-compose.yml dest: /opt/abc-retail/docker-compose.yml mode: "0644" - name: "Start ABC Retail application" shell: docker compose up -d args: chdir: /opt/abc-retail # ---- Verify Deployment ---- - name: "Wait for web server to be ready" wait_for: port: "{{ web_port }}" host: localhost delay: 5 timeout: 60 - name: "Test web server responds" uri: url: "http://localhost:{{ web_port }}" status_code: 200 register: health_check - name: "Display deployment result" debug: msg: "ABC Retail web server is HEALTHY at http://{{ inventory_hostname }}:{{ web_port }}" when: health_check.status == 200
This Ansible playbook automates the entire web server setup: system updates, security hardening (UFW firewall), Docker installation, and application deployment — in the correct order. Key concepts: become: yes — use sudo for all tasks. apt module — manage Ubuntu packages. ufw module — configure firewall (Ansible has modules for hundreds of tasks). {{ variables }} — parameterized values (the double brace syntax). wait_for — pause until a condition is met. uri — make an HTTP request to verify the site is up. debug — print a message when condition is met. Running this playbook on a blank Ubuntu server would fully configure it for ABC Retail in about 3–5 minutes without any manual steps.
6
Run the playbook with a dry-run first:
# Dry run (check mode) - shows what WOULD be done without making changes ansible-playbook -i inventory.ini setup-webserver.yml --check # Actual run ansible-playbook -i inventory.ini setup-webserver.yml
The --check flag runs in "check mode" — Ansible simulates all tasks and reports what would change, without actually making any changes. This is the Ansible equivalent of terraform plan. After reviewing the dry-run output, run without --check to apply. Watch the play output: each task shows OK (no change needed), CHANGED (task made a change), or FAILED (task encountered an error). Green OK means the system is already in the desired state — Ansible is idempotent.
PHASE 3 · STEP 1 Version Control the IaC Code with Git Workflow
1
Add all IaC files to Git and create the first proper commit:
cd ~/abc-retail-iac git add terraform/main.tf terraform/dev.tfvars git add ansible/inventory.ini ansible/setup-webserver.yml git status git commit -m "feat: add Terraform Docker provider config and Ansible web server playbook - Terraform: manages 2-replica web server + MariaDB via Docker provider - Ansible: automates full web server setup (UFW, Docker, app deployment) - Variables: separate dev.tfvars for environment-specific config Reviewed-by: Cloud Team Lead Tested-on: Ubuntu 22.04 LTS"
Commit messages should be informative — future engineers (including yourself) will read them to understand why a change was made. The format follows the "Conventional Commits" standard: type: short summary followed by a detailed body. The feat: type indicates a new feature. Other types include: fix: (bug fix), docs: (documentation), refactor: (code reorganization), chore: (maintenance). Good commit messages are a professional expectation in team environments.
2
Create a feature branch for adding a monitoring configuration:
git checkout -b feature/add-monitoring-config # Make changes, then merge back to main git add . git commit -m "feat: add Prometheus monitoring configuration" git checkout main git merge feature/add-monitoring-config
Feature branches are the core workflow in professional development: create a branch for each change, develop and test on that branch, then merge to main after review. This prevents unstable work from affecting the main codebase. In a team environment, branches are pushed to GitHub and opened as Pull Requests — which require peer review and automated tests to pass before they can be merged. This workflow applies equally to application code and IaC code.

3. Complete Configuration Templates

variables.tf — Variable Declarations with Descriptions and Validation
# variables.tf — All Terraform variable declarations variable "web_port" { description = "Port to expose the web application on" type = number default = 8080 validation { condition = var.web_port >= 1024 && var.web_port <= 65535 error_message = "web_port must be between 1024 and 65535." } } variable "web_replicas" { description = "Number of web server containers" type = number default = 2 validation { condition = var.web_replicas >= 1 && var.web_replicas <= 10 error_message = "web_replicas must be between 1 and 10." } } variable "environment" { description = "Deployment environment" type = string default = "dev" validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "environment must be one of: dev, staging, prod." } }

4. Deliverables Summary

📄 Files to Submit

  • terraform/main.tf (complete configuration)
  • terraform/variables.tf (variable declarations)
  • terraform/dev.tfvars and prod.tfvars
  • ansible/inventory.ini
  • ansible/setup-webserver.yml (complete playbook)
  • .gitignore (correct entries)
  • Git log showing commits (git log --oneline)
  • Screenshot: terraform plan output
  • Screenshot: terraform apply success output
  • Screenshot: ansible-playbook output (all tasks OK/CHANGED)
  • Screenshot: Website accessible after Ansible deployment

✅ Verification Checklist

  • Terraform installed (terraform version)
  • Git repo initialized with .gitignore
  • main.tf has all 6 resource types
  • terraform init completes without error
  • terraform plan shows resources to create
  • terraform apply creates all containers
  • Website accessible at port 8080 after apply
  • Scaling test: change replicas and re-apply
  • Ansible installed (ansible --version)
  • Inventory file configured with correct IPs
  • ansible all -m ping returns pong
  • Playbook runs without FAILED tasks
  • At least 2 Git commits with good messages

5. Why We Did This & What It Accomplishes

Strategic Intent & Operational Impact

Why We Did This

What This Accomplishes