AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible vs Terraform: Key Differences & When to Use Each (2026 Guide)

By Luca Berton · Published 2024-01-01 · Category: installation

Ansible vs Terraform comparison for 2026. Key differences in state management, language, approach, use cases, and when to use each — or both together.

Ansible vs Terraform: Key Differences & When to Use Each (2026 Guide)

Introduction

Ansible vs Terraform is one of the most common comparisons in the DevOps world. Both tools automate infrastructure, but they solve fundamentally different problems. Ansible is a configuration management and deployment tool. Terraform is an infrastructure provisioning tool.

The short answer: use both together. Terraform provisions your cloud infrastructure, Ansible configures and manages it. But understanding when to use each — and why — is critical for building reliable automation.

This guide provides a comprehensive 2026 comparison with practical examples, real-world patterns, and clear decision criteria.

See also: Ansible vs Terraform: Are They the Same? Key Differences Explained (2026)

Head-to-Head Comparison Table

| Feature | Ansible | Terraform | |---------|---------|-----------| | Primary Purpose | Configuration management + deployment | Infrastructure provisioning + lifecycle | | Language | YAML (playbooks) | HCL (HashiCorp Configuration Language) | | Approach | Procedural (step-by-step) | Declarative (desired state) | | State Management | Stateless | Stateful (terraform.tfstate) | | Agent Required | No (agentless via SSH/WinRM) | No (agentless via cloud APIs) | | Execution | Push-based (control node → targets) | API-based (control node → cloud APIs) | | Dry Run | --check --diff | terraform plan | | Rollback | Manual playbooks | terraform destroy + state rollback | | Secrets Management | Ansible Vault (built-in) | External (HashiCorp Vault, env vars) | | Cloud Providers | 70+ collections | 3,000+ providers | | Mutability | Mutable (in-place changes) | Immutable preferred (replace resources) | | Learning Curve | Lower (YAML is familiar) | Moderate (HCL is a new language) | | Enterprise Product | Ansible Automation Platform (Red Hat) | Terraform Cloud/Enterprise (HashiCorp) | | Open Source License | GPL v3 | BSL 1.1 (OpenTofu fork: MPL 2.0) | | Written In | Python | Go | | Windows Support | Native (WinRM/SSH) | Via cloud provider APIs | | Network Devices | 100+ platform modules | Limited | | Idempotency | Per-module (most modules are idempotent) | Built-in (declarative state convergence) |

Approach: Procedural vs Declarative

Ansible: Procedural (How to Get There)

Ansible playbooks describe the steps to reach the desired state. You tell Ansible what to do in order:

# Ansible: procedural — define steps
- name: Deploy web application
  hosts: webservers
  become: true
  tasks:
    - name: Install nginx
      ansible.builtin.apt:
        name: nginx
        state: present

- name: Deploy configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/sites-available/default mode: '0644' notify: reload nginx

- name: Deploy application code ansible.builtin.git: repo: https://github.com/myorg/myapp.git dest: /var/www/myapp version: "{{ app_version }}"

- name: Start nginx ansible.builtin.service: name: nginx state: started enabled: true

handlers: - name: reload nginx ansible.builtin.service: name: nginx state: reloaded

Terraform: Declarative (What the End State Should Be)

Terraform configurations describe the desired state. You tell Terraform what you want, and it figures out how to get there:

# Terraform: declarative — define desired state
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
  tags       = { Name = "production-vpc" }
}

resource "aws_subnet" "web" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24" availability_zone = "us-east-1a" }

resource "aws_instance" "web" { count = 3 ami = "ami-0c55b159cbfafe1f0" instance_type = "t3.medium" subnet_id = aws_subnet.web.id tags = { Name = "web-server-${count.index + 1}" } }

resource "aws_lb" "web" { name = "web-lb" internal = false load_balancer_type = "application" subnets = [aws_subnet.web.id] }

Key Insight

Terraform's declarative approach means it builds a dependency graph automatically. It knows the VPC must exist before the subnet, and the subnet before the instances. Ansible executes tasks in the order you write them — you manage dependencies yourself.

See also: Can Ansible Replace Terraform?

State Management: The Biggest Difference

Terraform: Stateful

Terraform maintains a state file (terraform.tfstate) that tracks every resource it manages:

# Terraform knows what exists
terraform plan
# Output:
# ~ aws_instance.web[0] will be updated in-place
#   - instance_type: "t3.small" → "t3.medium"
# Plan: 0 to add, 1 to change, 0 to destroy.

terraform apply # Apply the planned changes

Benefits of state: • Drift detection — identifies when reality differs from configuration • Change planning — preview exactly what will change before applying • Dependency tracking — knows the order to create/destroy resources • Resource mapping — connects config to real infrastructure

Risks of state: • State file can become corrupted • State must be shared in teams (remote backends like S3) • Sensitive data stored in state (secrets in plain text)

Ansible: Stateless

Ansible doesn't track state. Every playbook run connects to targets and evaluates the current state in real-time:

# Ansible checks current state each run
ansible-playbook site.yml --check --diff
# Output shows what WOULD change

ansible-playbook site.yml # Idempotent modules skip already-correct resources # "ok" = already in desired state, "changed" = modified

Benefits of stateless: • No state file to manage, share, or corrupt • Simpler mental model • Works on existing infrastructure without import • Each run is independent

Drawbacks: • No change planning (--check is approximate) • Can't track what it previously created • No automatic drift detection • Can't easily destroy "everything it created"

When to Use Ansible

Ansible is the right choice when your primary needs involve managing what's ON the servers:

1. Configuration Management

- name: Harden server security
  hosts: all
  become: true
  tasks:
    - name: Set SSH configuration
      ansible.builtin.lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^PermitRootLogin', line: 'PermitRootLogin no' }
        - { regexp: '^PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^MaxAuthTries', line: 'MaxAuthTries 3' }
      notify: restart sshd

- name: Configure firewall rules ansible.builtin.ufw: rule: allow port: "{{ item }}" proto: tcp loop: ['22', '80', '443']

2. Application Deployment

- name: Deploy application with zero downtime
  hosts: webservers
  serial: 1  # Rolling deployment
  tasks:
    - name: Remove from load balancer
      ansible.builtin.uri:
        url: "https://lb.example.com/api/deregister/{{ inventory_hostname }}"
        method: POST

- name: Deploy new version ansible.builtin.git: repo: https://github.com/myorg/app.git dest: /opt/app version: "{{ release_tag }}"

- name: Run migrations ansible.builtin.command: cmd: /opt/app/manage.py migrate run_once: true

- name: Restart application ansible.builtin.systemd: name: myapp state: restarted

- name: Wait for health check ansible.builtin.uri: url: "http://{{ inventory_hostname }}:8080/health" status_code: 200 register: health until: health.status == 200 retries: 30 delay: 5

- name: Re-register with load balancer ansible.builtin.uri: url: "https://lb.example.com/api/register/{{ inventory_hostname }}" method: POST

3. Network Device Automation

- name: Configure Cisco switches
  hosts: switches
  gather_facts: false
  tasks:
    - name: Configure VLANs
      cisco.ios.ios_vlans:
        config:
          - vlan_id: 100
            name: PRODUCTION
          - vlan_id: 200
            name: DEVELOPMENT

- name: Configure interfaces cisco.ios.ios_l3_interfaces: config: - name: Vlan100 ipv4: - address: 10.100.0.1/24

4. Ad-Hoc Operations and Troubleshooting

# Quick one-liners — unique to Ansible
ansible webservers -m shell -a "df -h / | tail -1"
ansible all -m ping
ansible databases -m service -a "name=postgresql state=restarted"
ansible webservers -m command -a "uptime"

See also: Ansible Troubleshooting: Fix Jinja2 Syntax & Inventory Errors

When to Use Terraform

Terraform is the right choice when your primary needs involve creating and managing the infrastructure itself:

1. Cloud Infrastructure Provisioning

# Complete AWS infrastructure
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"
  
  name = "production"
  cidr = "10.0.0.0/16"
  
  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
  
  enable_nat_gateway = true
  single_nat_gateway = false
}

resource "aws_rds_instance" "main" { engine = "postgres" engine_version = "16.2" instance_class = "db.r6g.xlarge" allocated_storage = 100 db_subnet_group_name = module.vpc.database_subnet_group_name backup_retention_period = 7 multi_az = true }

2. Multi-Cloud Deployments

# Consistent workflow across clouds
provider "aws" {
  region = "us-east-1"
}

provider "azurerm" { features {} }

provider "google" { project = "my-project" region = "us-central1" }

# Same resource patterns, different providers resource "aws_instance" "web_aws" { ... } resource "azurerm_virtual_machine" "web_azure" { ... } resource "google_compute_instance" "web_gcp" { ... }

3. Infrastructure Lifecycle Management

# Terraform excels at lifecycle operations
terraform plan          # Preview ALL changes
terraform apply         # Create/modify resources
terraform destroy       # Tear down everything
terraform import        # Import existing resources
terraform state mv      # Refactor without destroy
terraform taint         # Force resource recreation

The most production-proven pattern is Terraform provisions, Ansible configures:

Architecture

┌─────────────────────────────────────────────────┐
│                  Terraform                        │
│  Creates: VPCs, subnets, security groups,        │
│           EC2 instances, RDS, S3, Route53        │
│  Outputs: IP addresses, endpoints, credentials    │
└──────────────────────┬──────────────────────────┘
                       │
                       ▼
┌─────────────────────────────────────────────────┐
│                   Ansible                         │
│  Configures: packages, users, services, apps     │
│  Deploys: application code, configs, migrations  │
│  Manages: ongoing operations, updates, scaling   │
└─────────────────────────────────────────────────┘

Practical Example: Terraform + Ansible Pipeline

Step 1: Terraform provisions infrastructure

# main.tf
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"
  key_name      = "deploy-key"
  
  tags = {
    Name = "web-${count.index + 1}"
    Role = "webserver"
  }
}

# Output IPs for Ansible output "web_ips" { value = aws_instance.web[*].public_ip }

# Generate Ansible inventory automatically resource "local_file" "ansible_inventory" { content = templatefile("inventory.tpl", { web_ips = aws_instance.web[*].public_ip }) filename = "../ansible/inventory/hosts" }

Step 2: Ansible configures the servers

# configure.yml
- name: Configure web servers provisioned by Terraform
  hosts: webservers
  become: true
  tasks:
    - name: Install required packages
      ansible.builtin.apt:
        name:
          - nginx
          - python3
          - certbot
        state: present
        update_cache: true

- name: Deploy application ansible.builtin.git: repo: "{{ app_repo }}" dest: /opt/myapp version: "{{ app_version }}" notify: restart app

- name: Configure nginx ansible.builtin.template: src: nginx-site.conf.j2 dest: /etc/nginx/sites-available/myapp notify: reload nginx

- name: Setup SSL with certbot ansible.builtin.command: cmd: > certbot --nginx -d {{ domain_name }} --non-interactive --agree-tos --email {{ admin_email }} creates: /etc/letsencrypt/live/{{ domain_name }}

handlers: - name: restart app ansible.builtin.systemd: name: myapp state: restarted - name: reload nginx ansible.builtin.service: name: nginx state: reloaded

Step 3: CI/CD pipeline ties them together

# .gitlab-ci.yml
stages:
  - provision
  - configure
  - deploy

provision: stage: provision script: - cd terraform/ - terraform init - terraform plan -out=plan.tfplan - terraform apply plan.tfplan

configure: stage: configure script: - cd ansible/ - ansible-playbook -i inventory/hosts configure.yml

deploy: stage: deploy script: - cd ansible/ - ansible-playbook -i inventory/hosts deploy.yml -e "app_version=${CI_COMMIT_TAG}"

Dynamic Inventory from Cloud

Instead of static inventory files, use Ansible's cloud dynamic inventory:

# aws_ec2.yml — auto-discover Terraform-created instances
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
filters:
  tag:Role:
    - webserver
    - database
keyed_groups:
  - key: tags.Role
    prefix: role
compose:
  ansible_host: public_ip_address

Decision Framework

Use this flowchart to decide which tool to use:

| Your Primary Task | Best Tool | Why | |-------------------|-----------|-----| | Create cloud VMs, networks, databases | Terraform | Declarative provisioning + state tracking | | Install packages, configure services | Ansible | Agentless, push-based, idempotent modules | | Deploy application code | Ansible | Rolling deploys, health checks, orchestration | | Manage infrastructure lifecycle | Terraform | Plan/apply/destroy workflow | | Configure network devices (Cisco, etc.) | Ansible | 100+ network platform modules | | Multi-cloud infrastructure | Terraform | Consistent provider abstraction | | Quick one-off tasks | Ansible | Ad-hoc commands, no setup needed | | Kubernetes cluster creation | Terraform | EKS/AKS/GKE provider modules | | Kubernetes app deployment | Ansible | kubernetes.core collection | | Manage existing servers (no cloud) | Ansible | SSH-based, works on any Linux/Windows | | Infrastructure drift detection | Terraform | State comparison built-in | | Compliance enforcement | Both | Terraform for infra, Ansible for OS config | | CI/CD pipeline integration | Both | Terraform provisions, Ansible configures |

Common Misconceptions

"Ansible can replace Terraform"

Ansible has cloud modules (amazon.aws, azure.azcollection, google.cloud) that can provision infrastructure. However, Ansible lacks: • State management — can't track what it created • Dependency graphs — can't automatically determine resource ordering • Plan/preview--check mode is approximate, not guaranteed • Destroy capability — no equivalent to terraform destroy

For small setups, Ansible alone works fine. At scale, the lack of state becomes painful.

"Terraform can replace Ansible"

Terraform has provisioner blocks and user_data for basic configuration:

resource "aws_instance" "web" {
  ami           = "ami-abc123"
  instance_type = "t3.medium"
  
  # Basic — but fragile
  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
  EOF
}

This works for simple bootstrap, but Terraform cannot: • Manage ongoing configuration changes • Perform rolling deployments • Run ad-hoc operational tasks • Manage network devices • Handle complex multi-step orchestration

"You must choose one or the other"

Most production teams use both. The real question isn't "Ansible vs Terraform" but "where does one end and the other begin?"

OpenTofu: The Open Source Terraform Fork

Since HashiCorp changed Terraform's license to BSL 1.1 in August 2023, the open-source community created OpenTofu (MPL 2.0 license). Everything in this comparison applies equally to OpenTofu, which maintains API compatibility with Terraform.

# OpenTofu is a drop-in replacement
tofu init
tofu plan
tofu apply

Ansible's relationship with OpenTofu is the same as with Terraform — they're complementary tools in different domains.

Cost Comparison (2026)

| Aspect | Ansible | Terraform | |--------|---------|-----------| | Open Source | Free (ansible-core) | Free (Terraform CLI / OpenTofu) | | Enterprise | AAP: ~$14,000/yr (100 nodes) | Terraform Cloud: Free-$70/user/mo | | Cloud Pricing | N/A (runs on your infra) | State storage on your backend | | Support | Red Hat subscription | HashiCorp support plans |

FAQ

Can Ansible do everything Terraform does?

Ansible can provision cloud resources via modules like amazon.aws.ec2_instance, but it lacks Terraform's state management, dependency graphs, and plan workflow. For simple setups it works; at scale, Terraform is superior for provisioning.

Can Terraform do everything Ansible does?

No. Terraform excels at infrastructure provisioning but is not designed for configuration management, application deployment, or managing existing servers. Use Ansible for everything that happens after infrastructure exists.

Is Terraform replacing Ansible?

No. The industry trend is using both tools together. Google Trends shows consistent demand for both. They solve different problems and complement each other.

Which is easier to learn?

Ansible is generally easier because YAML is familiar to most DevOps engineers. Terraform requires learning HCL, understanding state management, and grasping the plan/apply lifecycle. However, both have excellent documentation.

What about Pulumi?

Pulumi replaces HCL with real programming languages (Python, TypeScript, Go). It competes with Terraform for infrastructure provisioning but doesn't replace Ansible for configuration management.

Should I use Ansible or Terraform for Kubernetes?

Use Terraform to create the cluster (EKS, AKS, GKE). Use Ansible with the kubernetes.core collection to deploy applications and manage configurations inside the cluster.

What about Ansible with AWS CloudFormation?

CloudFormation is AWS-only and uses JSON/YAML templates. Terraform is multi-cloud. Ansible can orchestrate both: use the amazon.aws.cloudformation module to deploy stacks, or Terraform for multi-cloud.

Which has better community support?

Both have massive communities. Ansible has 60,000+ GitHub stars and 900+ built-in modules. Terraform has 40,000+ stars and 3,000+ providers. Both have active forums, extensive documentation, and regular releases.

Conclusion

Ansible and Terraform are not competitors — they're complementary tools. The most effective infrastructure teams use both: • Terraform provisions and manages cloud infrastructure (VMs, networks, databases, DNS) • Ansible configures and manages everything on those resources (packages, services, applications, users)

If you can only choose one: • Choose Ansible if your primary need is managing existing servers, deploying applications, or automating network devices • Choose Terraform if your primary need is provisioning cloud infrastructure from scratch

Best practice for 2026: Use Terraform for infrastructure-as-code provisioning, Ansible for configuration management and deployment, and integrate both in your CI/CD pipeline.

Related Articles

Ansible vs Kubernetes: Comparison GuideAnsible AWS Complete GuideAnsible CI/CD Pipeline IntegrationAnsible for Windows Complete GuideWhat is Ansible AWXAnsible Terraform Integration Guide

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home