import ComparisonTable from ’../../components/ComparisonTable.astro’;
Ansible and Terraform are both infrastructure automation tools, but they solve different problems. Terraform provisions infrastructure (create servers, networks, databases). Ansible configures what’s already running (install software, manage config files, deploy applications).
Quick Verdict
Choose Terraform if: You’re creating cloud resources (EC2 instances, VPCs, RDS databases, Kubernetes clusters) and need to track and manage their state over time.
Choose Ansible if: You’re configuring servers, deploying applications, or orchestrating multi-step tasks on existing infrastructure.
Use both: Terraform creates the infrastructure; Ansible configures it. This is the most common production pattern.
Core Philosophy Difference
<ComparisonTable headers={[“Dimension”, “Terraform”, “Ansible”]} rows={[ [“Primary use”, “Infrastructure provisioning”, “Configuration management”], [“Approach”, “Declarative (desired state)”, “Procedural (step by step)”], [“State management”, “State file (terraform.tfstate)”, “Stateless (idempotent tasks)”], [“Language”, “HCL (HashiCorp Config Language)”, “YAML (playbooks)”], [“Agentless”, “Yes”, “Yes (SSH)”], [“Cloud-native”, “Yes (providers for all clouds)”, “Good (cloud modules)”], [“OS config”, “Not designed for this”, “Excellent”], [“Application deploy”, “Limited”, “Excellent”], [“Idempotency”, “Built-in (plan/apply model)”, “Module-dependent”], [“Community”, “Large (Registry)”, “Very large (Galaxy)”], [“Enterprise”, “HCP Terraform / Terraform Enterprise”, “Ansible Automation Platform”], ]} />
Terraform: Infrastructure Provisioning
Terraform describes the desired final state of your infrastructure:
main.tf — AWS infrastructure:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
}
}
provider "aws" {
region = var.aws_region
}
# VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${var.project}-vpc"
Environment = var.environment
}
}
# Public subnets
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-public-${count.index + 1}"
}
}
# RDS Database
resource "aws_db_instance" "postgres" {
identifier = "${var.project}-db"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t3.medium"
allocated_storage = 20
storage_type = "gp3"
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 7
skip_final_snapshot = false
tags = {
Environment = var.environment
}
}
# ECS Service (application)
resource "aws_ecs_service" "app" {
name = "${var.project}-app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
desired_count = var.app_count
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.app.id]
assign_public_ip = false
}
load_balancer {
target_group_arn = aws_lb_target_group.app.arn
container_name = "app"
container_port = 8080
}
}
Terraform workflow:
# Initialize (download providers)
terraform init
# Preview changes (no infrastructure modified)
terraform plan -var-file=production.tfvars
# Apply changes
terraform apply -var-file=production.tfvars
# Show current state
terraform show
# Destroy all resources
terraform destroy
# Import existing resource into state
terraform import aws_s3_bucket.example my-existing-bucket
# Check what will be affected by a change
terraform plan -target=aws_ecs_service.app
State management:
# terraform.tfstate tracks current reality
# Running `terraform plan` compares:
# - Desired state (your .tf files)
# - Actual state (tfstate file)
# - Real infrastructure (API calls)
# If someone manually deleted a resource:
# terraform plan shows: "will be created" (drift detected)
# Remote state (team collaboration)
backend "s3" {
bucket = "my-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-state-lock" # Prevent concurrent applies
encrypt = true
}
Ansible: Configuration Management
Ansible describes tasks to execute on servers:
inventory.yml — what servers to manage:
all:
children:
webservers:
hosts:
web1:
ansible_host: 10.0.1.10
web2:
ansible_host: 10.0.1.11
vars:
ansible_user: ubuntu
ansible_ssh_private_key_file: ~/.ssh/prod.pem
databases:
hosts:
db1:
ansible_host: 10.0.2.10
playbooks/web-server.yml — configure web servers:
---
- name: Configure web servers
hosts: webservers
become: true # sudo
vars:
app_user: appuser
app_dir: /opt/myapp
nginx_port: 80
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install required packages
ansible.builtin.apt:
name:
- nginx
- python3
- python3-pip
state: present
- name: Create application user
ansible.builtin.user:
name: "{{ app_user }}"
shell: /bin/bash
create_home: true
- name: Create application directory
ansible.builtin.file:
path: "{{ app_dir }}"
state: directory
owner: "{{ app_user }}"
group: "{{ app_user }}"
mode: '0755'
- name: Deploy application from S3
amazon.aws.aws_s3:
bucket: my-app-artifacts
object: "app-{{ app_version }}.tar.gz"
dest: "/tmp/app.tar.gz"
mode: get
- name: Extract application
ansible.builtin.unarchive:
src: "/tmp/app.tar.gz"
dest: "{{ app_dir }}"
remote_src: true
owner: "{{ app_user }}"
- name: Install Python dependencies
ansible.builtin.pip:
requirements: "{{ app_dir }}/requirements.txt"
virtualenv: "{{ app_dir }}/venv"
- name: Configure nginx
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/myapp
mode: '0644'
notify: Restart nginx
- name: Enable nginx site
ansible.builtin.file:
src: /etc/nginx/sites-available/myapp
dest: /etc/nginx/sites-enabled/myapp
state: link
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Running Ansible:
# Run playbook on all web servers
ansible-playbook -i inventory.yml playbooks/web-server.yml
# Dry run (check mode — no changes made)
ansible-playbook -i inventory.yml playbooks/web-server.yml --check
# Run only specific tasks (by tag)
ansible-playbook -i inventory.yml playbooks/web-server.yml --tags nginx
# Run on specific host
ansible-playbook -i inventory.yml playbooks/web-server.yml --limit web1
# Ad-hoc command (no playbook needed)
ansible webservers -i inventory.yml -m service -a "name=nginx state=restarted"
# Check disk usage on all servers
ansible all -i inventory.yml -m command -a "df -h"
Using Both Together
The most common production pattern:
Step 1: Terraform creates infrastructure
- VPC, subnets, security groups
- EC2 instances (running Ubuntu)
- RDS database
- Load balancer
- Outputs: IP addresses, endpoint URLs
Step 2: Ansible configures infrastructure
- Installs application dependencies on EC2 instances
- Deploys application code
- Configures nginx, systemd services
- Sets up monitoring agents (Prometheus node_exporter)
- Applies security hardening (fail2ban, ufw rules)
Terraform → Ansible handoff:
# Terraform output — pass to Ansible
output "web_server_ips" {
value = aws_instance.web[*].public_ip
}
output "db_endpoint" {
value = aws_db_instance.postgres.endpoint
}
# Generate Ansible inventory from Terraform output
terraform output -json web_server_ips | \
python3 scripts/generate_inventory.py > inventory/production.yml
# Run Ansible with Terraform-generated inventory
ansible-playbook -i inventory/production.yml playbooks/deploy.yml \
-e "db_host=$(terraform output -raw db_endpoint)"
When to Choose Each
Use Terraform for:
- Creating AWS/GCP/Azure resources
- Managing Kubernetes clusters (EKS, GKE, AKS)
- Database instances, object storage, networking
- Any infrastructure that needs state tracking
- Multi-cloud environments
Use Ansible for:
- Installing and configuring software on servers
- Deploying application code
- Managing configuration files and templates
- Orchestrating deployment workflows
- Server patching and security hardening
- Database migrations
- Any task-based automation across many servers
Use both: Most production infrastructure uses Terraform for provisioning and Ansible for configuration.
Bottom Line
Ansible and Terraform aren’t competitors — they’re complements. Terraform’s declarative state model is ideal for managing cloud resources that persist over time. Ansible’s procedural playbooks excel at configuring servers and deploying applications. The most resilient infrastructure teams use Terraform to define what resources exist and Ansible to define how those resources are configured.