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
| Aspect | Terraform | Ansible |
|---|---|---|
| Type | Infrastructure Provisioning | Configuration Management |
| Model | Declarative | Imperative (with declarative features) |
| State | State files (.tfstate) | Stateless |
| Idempotency | Native | Implemented via modules |
| Learning Curve | Moderate | Gentle |
| Use Case | Infrastructure as Code | Configuration & 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
- State management: Tracks infrastructure state
- Plan & Apply: Preview changes before applying
- Modularity: Reusable, composable modules
- Multi-cloud: Single syntax for multiple providers
- Immutability: Infrastructure is versioned
Terraform Weaknesses
- State complexity: Managing .tfstate files can be tricky
- Steep learning curve: HCL syntax takes time to master
- Not ideal for OS-level changes: Terraform focuses on infrastructure
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
- Agentless: No software needed on target systems
- Simple syntax: YAML is easy to learn
- Powerful: Can orchestrate complex multi-step processes
- Idempotent modules: Many modules are idempotent by default
- Flexible: Push and pull models both supported
Ansible Weaknesses
- No state tracking: Doesn’t maintain infrastructure state
- Performance: Slower than some alternatives for large-scale deployments
- Error handling: Can be complex in large playbooks
- Not ideal for provisioning: Better for configuration than creation
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
| Scenario | Use Tool | Reason |
|---|---|---|
| Create VPC, subnets, load balancers | Terraform | Infrastructure provisioning |
| Launch EC2 instances | Terraform | Cloud resource creation |
| Install and configure OS packages | Ansible | OS-level configuration |
| Deploy application code | Ansible | Application deployment |
| Setup CI/CD pipelines | Ansible | Complex orchestration |
| Manage DNS records | Terraform | Cloud resource management |
| Configure firewalls/security groups | Terraform | Infrastructure definition |
| Patch servers | Ansible | OS management |
| Set up database backups | Terraform | Infrastructure policy |
| Monitor and restart services | Ansible | Operational 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:
- Terraform for declaring what your infrastructure looks like
- Ansible for configuring how your systems behave
Master both, and you’ll have a powerful DevOps toolkit.
What’s your preferred IaC approach? Share your experiences in the comments!