Automation Crunch 😎
September 20, 2024

Terraform vs Ansible: When to Use Each for Infrastructure as Code

Posted on September 20, 2024  •  6Β minutes  • 1248Β words

Introduction

One of the most common questions in DevOps is: “Should I use Terraform or Ansible?”

The answer? Both. But understanding when and how to use each is crucial for building scalable, maintainable infrastructure.

In this article, I’ll break down the differences, share real-world scenarios, and show you how to leverage both tools effectively.

Quick Comparison

AspectTerraformAnsible
TypeInfrastructure ProvisioningConfiguration Management
ModelDeclarativeImperative (with declarative features)
StateState files (.tfstate)Stateless
IdempotencyNativeImplemented via modules
Learning CurveModerateGentle
Use CaseInfrastructure as CodeConfiguration & Orchestration

Terraform: Infrastructure Provisioning

What is Terraform?

Terraform is a tool for building, changing, and versioning infrastructure safely. It defines infrastructure as code using HCL (HashiCorp Configuration Language).

When to Use Terraform

βœ… Provisioning cloud infrastructure

# AWS EC2 Instance
resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
  
  tags = {
    Name = "WebServer"
  }
}

βœ… Creating managed services

# AWS RDS Database
resource "aws_db_instance" "main" {
  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  name                 = "mydb"
  
  backup_retention_period = 7
  skip_final_snapshot     = false
}

βœ… Multi-cloud deployments

# AWS + Azure + GCP infrastructure
module "aws_infrastructure" {
  source = "./modules/aws"
}

module "azure_infrastructure" {
  source = "./modules/azure"
}

module "gcp_infrastructure" {
  source = "./modules/gcp"
}

Terraform Strengths

Terraform Weaknesses

Real-World Example: Multi-Environment Setup

# environments/prod/main.tf
terraform {
  required_version = ">= 1.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

variable "environment" {
  type    = string
  default = "production"
}

variable "instance_count" {
  type    = number
  default = 3
}

# VPC
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  
  tags = {
    Name = "${var.environment}-vpc"
  }
}

# Subnets
resource "aws_subnet" "public" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = "10.0.${count.index}.0/24"
  availability_zone = data.aws_availability_zones.available.names[count.index]
  
  tags = {
    Name = "${var.environment}-public-subnet-${count.index + 1}"
  }
}

# Security Group
resource "aws_security_group" "alb" {
  name        = "${var.environment}-alb-sg"
  description = "Security group for ALB"
  vpc_id      = aws_vpc.main.id
  
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# Application Load Balancer
resource "aws_lb" "main" {
  name               = "${var.environment}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = aws_subnet.public[*].id
}

# EC2 Instances
resource "aws_instance" "app" {
  count                = var.instance_count
  ami                  = data.aws_ami.ubuntu.id
  instance_type        = "t3.medium"
  iam_instance_profile = aws_iam_instance_profile.app.name
  
  subnet_id              = aws_subnet.public[count.index % length(aws_subnet.public)].id
  vpc_security_group_ids = [aws_security_group.app.id]
  
  user_data = base64encode(file("${path.module}/user_data.sh"))
  
  tags = {
    Name = "${var.environment}-app-${count.index + 1}"
  }
}

# Outputs
output "alb_dns_name" {
  description = "DNS name of the ALB"
  value       = aws_lb.main.dns_name
}

output "instance_ids" {
  description = "IDs of EC2 instances"
  value       = aws_instance.app[*].id
}

Ansible: Configuration Management

What is Ansible?

Ansible is an agentless automation tool that uses SSH (or other protocols) to configure systems and execute tasks on remote machines.

When to Use Ansible

βœ… Configuring OS-level settings

---
- name: Configure web servers
  hosts: web_servers
  tasks:
    - name: Install packages
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - nginx
        - python3
        - git
    
    - name: Start nginx service
      systemd:
        name: nginx
        state: started
        enabled: yes

βœ… Application deployment

---
- name: Deploy Flask application
  hosts: app_servers
  vars:
    app_version: "1.2.3"
  tasks:
    - name: Clone repository
      git:
        repo: "https://github.com/myorg/myapp.git"
        dest: "/opt/myapp"
        version: "{{ app_version }}"
    
    - name: Install dependencies
      pip:
        requirements: "/opt/myapp/requirements.txt"
        virtualenv: "/opt/myapp/venv"
    
    - name: Configure environment
      template:
        src: env.j2
        dest: "/opt/myapp/.env"
      notify: restart app
    
    - name: Copy systemd unit file
      copy:
        src: myapp.service
        dest: "/etc/systemd/system/"
      notify: reload systemd
  
  handlers:
    - name: restart app
      systemd:
        name: myapp
        state: restarted
    
    - name: reload systemd
      systemd:
        daemon_reload: yes

βœ… Infrastructure orchestration

---
- name: Setup complete infrastructure
  hosts: localhost
  tasks:
    - name: Provision infrastructure with Terraform
      terraform:
        project_path: "/path/to/terraform"
        state: present
      register: tf_result
    
    - name: Configure application on new instances
      add_host:
        name: "{{ item }}"
        groups: newly_provisioned
      loop: "{{ tf_result.outputs.instance_ips.value }}"
    
    - name: Configure newly provisioned instances
      import_tasks: configure_app.yml

Ansible Strengths

Ansible Weaknesses

Best Practice: Combining Terraform & Ansible

Architecture Pattern

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚     Terraform: Provision Infrastructure  β”‚
β”‚  (EC2, RDS, VPC, Load Balancers, etc.)   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                  β”‚
                  β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   Ansible: Configure & Deploy Apps       β”‚
β”‚  (Install software, configure services)  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Complete Example: Terraform + Ansible Workflow

Step 1: Provision with Terraform

# main.tf
resource "aws_instance" "web" {
  count                = 3
  ami                  = "ami-0c55b159cbfafe1f0"
  instance_type        = "t2.micro"
  vpc_security_group_ids = [aws_security_group.web.id]
  
  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

output "instance_ips" {
  value = aws_instance.web[*].private_ip
}

# Inventory file for Ansible
resource "local_file" "ansible_inventory" {
  content = templatefile("${path.module}/inventory.tpl", {
    servers = aws_instance.web[*].private_ip
  })
  filename = "${path.module}/../ansible/inventory.ini"
}

Step 2: Trigger Ansible from Terraform

# ansible.tf
resource "null_resource" "run_ansible" {
  provisioner "local-exec" {
    command = "cd ../ansible && ansible-playbook -i inventory.ini configure.yml"
  }
  
  depends_on = [
    aws_instance.web,
    local_file.ansible_inventory
  ]
}

Step 3: Configure with Ansible

# configure.yml
---
- name: Configure web servers
  hosts: web_servers
  become: yes
  tasks:
    - name: Update package cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
    
    - name: Install required packages
      apt:
        name: "{{ item }}"
        state: present
      loop:
        - nginx
        - curl
        - git
        - python3-pip
    
    - name: Deploy web application
      git:
        repo: "https://github.com/myorg/webapp.git"
        dest: "/var/www/myapp"
        version: main
    
    - name: Install Python dependencies
      pip:
        requirements: "/var/www/myapp/requirements.txt"
        virtualenv: "/var/www/myapp/venv"
    
    - name: Configure Nginx
      template:
        src: nginx.conf.j2
        dest: "/etc/nginx/sites-available/myapp"
      notify: reload nginx
    
    - name: Enable Nginx site
      file:
        src: "/etc/nginx/sites-available/myapp"
        dest: "/etc/nginx/sites-enabled/myapp"
        state: link
      notify: reload nginx
    
    - name: Start Nginx
      systemd:
        name: nginx
        state: started
        enabled: yes
  
  handlers:
    - name: reload nginx
      systemd:
        name: nginx
        state: reloaded

Decision Matrix

ScenarioUse ToolReason
Create VPC, subnets, load balancersTerraformInfrastructure provisioning
Launch EC2 instancesTerraformCloud resource creation
Install and configure OS packagesAnsibleOS-level configuration
Deploy application codeAnsibleApplication deployment
Setup CI/CD pipelinesAnsibleComplex orchestration
Manage DNS recordsTerraformCloud resource management
Configure firewalls/security groupsTerraformInfrastructure definition
Patch serversAnsibleOS management
Set up database backupsTerraformInfrastructure policy
Monitor and restart servicesAnsibleOperational management

Pro Tips

1. Store Terraform State Remotely

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

2. Use Ansible Vault for Secrets

ansible-vault encrypt secrets.yml
ansible-playbook -i inventory.ini playbook.yml --ask-vault-pass

3. Version Everything

git tag -a "v1.0-prod" -m "Production infrastructure v1.0"

4. Use Tags in Playbooks

- name: Deploy app
  hosts: app_servers
  tags: deployment
  tasks:
    # tasks here

# Run specific tags
# ansible-playbook playbook.yml --tags deployment

Conclusion

Terraform and Ansible are complementary tools:

Master both, and you’ll have a powerful DevOps toolkit.


What’s your preferred IaC approach? Share your experiences in the comments!

Resources

Follow me

You can find me on