Skip to content
Codeloom
DevOps

Ansible vs Terraform: Configuration vs Infrastructure

Compare Ansible and Terraform for DevOps automation. Learn when to use each tool, how they differ in approach, and how to combine them effectively.

·9 min read · By Codeloom
Intermediate 13 min read

What you'll learn

  • How Terraform and Ansible differ in purpose and approach
  • Declarative vs procedural infrastructure management
  • When to use each tool and when to combine them
  • Practical examples of both tools solving the same problem

Prerequisites

  • Basic understanding of cloud infrastructure
  • Familiarity with YAML or HCL syntax

Two Tools, Different Jobs

Terraform and Ansible are both automation tools used in DevOps, but they solve different problems. Terraform is an infrastructure provisioning tool. It creates and manages cloud resources like servers, networks, databases, and load balancers. Ansible is a configuration management tool. It installs software, manages files, and configures systems that already exist.

The confusion arises because both tools can do some of each other’s work. Terraform can run scripts on servers after creating them. Ansible can create cloud resources using its cloud modules. But each tool is purpose-built for one side of the equation, and using the right tool for the right job gives you better results.

Core Differences

AspectTerraformAnsible
Primary purposeProvision infrastructureConfigure systems
LanguageHCL (HashiCorp Configuration Language)YAML (playbooks)
ApproachDeclarativeProcedural (with declarative modules)
State managementState file tracks resourcesStateless (no state file)
Agent requirementNone (API-driven)None (SSH/WinRM)
IdempotencyBuilt-in via state comparisonModule-dependent
ExecutionPlan then applyExecute tasks sequentially
Drift detectionYes (plan shows drift)Limited

Declarative vs Procedural

Terraform is declarative. You describe the desired end state and Terraform figures out what changes are needed:

# Terraform: "I want 3 EC2 instances"
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"

  tags = {
    Name = "web-server-${count.index + 1}"
  }
}

If you change count from 3 to 5, Terraform creates 2 more instances. If you change it from 5 to 3, Terraform destroys 2 instances. You never tell Terraform how to get there; you only describe where you want to be.

Ansible is procedural. You write a sequence of tasks that execute in order:

# Ansible: "Do these steps on the servers"
- name: Configure web servers
  hosts: webservers
  become: true
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Copy nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: restart nginx

    - name: Ensure nginx is running
      service:
        name: nginx
        state: started
        enabled: true

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted

Ansible tasks run top to bottom. Each module (like apt, template, service) is individually idempotent, meaning running it twice produces the same result. But the overall playbook is a sequence of steps, not a state declaration.

State Management

Terraform maintains a state file that maps your configuration to real-world resources. This file is critical. It tells Terraform which resources it manages, what their current properties are, and what needs to change.

# View current state
terraform state list

# Output:
# aws_instance.web[0]
# aws_instance.web[1]
# aws_instance.web[2]
# aws_vpc.main
# aws_subnet.public

This state file enables powerful features:

# See what will change before applying
terraform plan

# Output:
# ~ aws_instance.web[0]
#     instance_type: "t3.medium" -> "t3.large"
# Plan: 0 to add, 1 to change, 0 to destroy.

The downside is that state must be stored securely and shared across your team. Remote backends like S3, GCS, or Terraform Cloud solve this:

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

Ansible has no state file. Each time a playbook runs, Ansible connects to the target hosts, checks current conditions, and applies changes. This simplicity is an advantage for configuration management but a limitation for infrastructure provisioning, because Ansible cannot track what it previously created.

Where Each Tool Excels

Terraform Strengths

Infrastructure lifecycle management. Terraform tracks every resource it creates and can update or destroy them. Renaming a resource, changing its type, or tearing down an entire environment is straightforward:

# Destroy all infrastructure
terraform destroy

# Destroy a specific resource
terraform destroy -target=aws_instance.web[2]

Dependency graph. Terraform automatically determines the order in which resources must be created. A subnet depends on a VPC, a security group depends on the VPC, and an EC2 instance depends on both:

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id  # implicit dependency
  cidr_block = "10.0.1.0/24"
}

resource "aws_instance" "web" {
  subnet_id     = aws_subnet.public.id  # implicit dependency
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"
}

Terraform creates the VPC first, then the subnet, then the instance. No manual ordering required.

Multi-cloud support. Terraform providers exist for AWS, GCP, Azure, Kubernetes, GitHub, Datadog, PagerDuty, and hundreds of other services. The same workflow applies everywhere.

Plan before apply. terraform plan shows exactly what will change before any action is taken. This is a safety net that Ansible lacks for infrastructure changes.

Ansible Strengths

System configuration. Ansible excels at tasks that happen inside a server: installing packages, managing users, deploying application code, configuring services, and managing files.

- name: Deploy application
  hosts: app_servers
  become: true
  vars:
    app_version: "2.5.0"
  tasks:
    - name: Create app user
      user:
        name: appuser
        system: true
        shell: /usr/sbin/nologin

    - name: Download application
      get_url:
        url: "https://releases.example.com/app-{{ app_version }}.tar.gz"
        dest: /opt/app/app.tar.gz
        checksum: "sha256:abc123..."

    - name: Extract application
      unarchive:
        src: /opt/app/app.tar.gz
        dest: /opt/app/
        remote_src: true

    - name: Configure application
      template:
        src: app-config.yml.j2
        dest: /opt/app/config.yml
        owner: appuser
        mode: '0644'
      notify: restart app

    - name: Ensure app service is running
      systemd:
        name: myapp
        state: started
        enabled: true

No agent required. Ansible connects over SSH (or WinRM for Windows). There is nothing to install on managed hosts. This makes it easy to start managing existing servers immediately.

Ad-hoc commands. Run one-off commands across your fleet without writing a playbook:

# Check disk space on all web servers
ansible webservers -m shell -a "df -h /"

# Restart a service everywhere
ansible all -m service -a "name=nginx state=restarted" --become

# Copy a file to all hosts
ansible all -m copy -a "src=hotfix.py dest=/opt/app/hotfix.py" --become

Inventory management. Ansible inventories can be static files or dynamic scripts that query your cloud provider:

# Static inventory
[webservers]
web1.example.com
web2.example.com

[databases]
db1.example.com

[webservers:vars]
nginx_worker_processes=4
# Dynamic inventory from AWS
ansible-inventory -i aws_ec2.yml --list

Solving the Same Problem with Each Tool

Let’s set up a web server with both tools to see the difference.

Terraform Approach

# main.tf - Create the infrastructure AND configure it
resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"
  key_name      = "my-key"

  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl enable nginx
    systemctl start nginx
    echo "<h1>Hello from Terraform</h1>" > /var/www/html/index.html
  EOF

  tags = {
    Name = "web-server"
  }
}

resource "aws_security_group" "web" {
  name = "web-sg"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

Terraform creates the security group and EC2 instance. The user_data script runs on first boot to install nginx. This works, but the configuration is baked into a bash script inside the Terraform file. It is not reusable, testable, or easy to modify without recreating the instance.

Ansible Approach

Assuming the server already exists:

# playbook.yml
- name: Configure web server
  hosts: web_servers
  become: true
  tasks:
    - name: Install nginx
      apt:
        name: nginx
        state: present
        update_cache: true

    - name: Deploy index page
      copy:
        content: "<h1>Hello from Ansible</h1>"
        dest: /var/www/html/index.html
        mode: '0644'

    - name: Ensure nginx is running
      service:
        name: nginx
        state: started
        enabled: true

This is clean and reusable. You can run it repeatedly. If someone manually changes the index page, Ansible reverts it. But Ansible did not create the server or the security group.

Combining Terraform and Ansible

The best practice is to use both tools together. Terraform provisions infrastructure, then Ansible configures it.

Approach 1: Terraform Outputs as Ansible Inventory

# Terraform outputs the server IP
output "web_server_ip" {
  value = aws_instance.web.public_ip
}
# After terraform apply, generate an Ansible inventory
terraform output -raw web_server_ip > inventory.ini

# Run Ansible against the new server
ansible-playbook -i inventory.ini playbook.yml

Approach 2: Dynamic Inventory

Use Ansible’s AWS dynamic inventory plugin to discover instances created by Terraform:

# aws_ec2.yml (Ansible inventory plugin)
plugin: aws_ec2
regions:
  - us-east-1
filters:
  tag:ManagedBy: terraform
  tag:Role: webserver
keyed_groups:
  - key: tags.Environment
    prefix: env
ansible-playbook -i aws_ec2.yml playbook.yml

Approach 3: Terraform Provisioners (Use Sparingly)

Terraform can invoke Ansible directly, though HashiCorp considers provisioners a last resort:

resource "aws_instance" "web" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"

  provisioner "local-exec" {
    command = "ansible-playbook -i '${self.public_ip},' playbook.yml"
  }
}

This couples the tools tightly and makes the Terraform apply slower. Prefer the inventory-based approaches.

Decision Guide

Use Terraform when:

  • Creating, modifying, or destroying cloud resources.
  • Managing infrastructure lifecycle (VPCs, databases, load balancers, DNS).
  • You need a plan/apply workflow with drift detection.
  • Working across multiple cloud providers.

Use Ansible when:

  • Installing and configuring software on servers.
  • Managing users, files, packages, and services.
  • Running ad-hoc commands across a fleet.
  • Deploying application code.
  • Handling tasks that change frequently (config updates, deployments).

Use both when:

  • You need infrastructure provisioning and system configuration.
  • Terraform creates the servers; Ansible configures them.
  • This gives you the best of both tools without fighting against their design.

Wrapping Up

Terraform and Ansible are complementary, not competing, tools. Terraform manages the lifecycle of infrastructure resources through a declarative state-based approach. Ansible manages the configuration of those resources through procedural task execution. Using Terraform for provisioning and Ansible for configuration gives you a clean separation of concerns, where each tool handles what it does best. The most effective DevOps teams adopt both tools and draw a clear line between infrastructure (Terraform) and configuration (Ansible).