Ansible for Configuration Management
Terraform provisions infrastructure — it creates VMs, networking, databases, and cloud resources. What it doesn't do well is configure what runs on those VMs after they exist: installing packages, writing config files, managing services, and deploying application code. Ansible fills that gap. Together, Terraform and Ansible cover the full lifecycle: Terraform provisions the machine, Ansible configures it.
What Is Ansible?
Ansible is an agentless automation tool written in Python. "Agentless" means it connects to target machines over SSH (or WinRM for Windows) and executes tasks directly — no daemon or agent process needs to be installed on managed nodes.
Key properties:
- Agentless — only SSH and Python are required on targets
- Idempotent — running a playbook twice should produce the same result as running it once
- Push-based — the control node pushes tasks to managed nodes; targets don't reach out
- YAML playbooks — human-readable task definitions
- Huge module library — 3,000+ modules for packages, files, services, cloud APIs, databases, and more
Ansible vs Terraform
| Terraform | Ansible | |
|---|---|---|
| Primary use | Provision cloud infrastructure | Configure servers, deploy apps |
| Model | Declarative (desired state) | Procedural (ordered tasks), idempotent |
| State | State file tracks resources | No persistent state — re-reads target on each run |
| Targets | Cloud provider APIs | Servers via SSH |
| Config format | HCL | YAML |
| Language | Terraform/OpenTofu | Python (runtime), YAML (playbooks) |
| Cloud provisioning | Excellent | Possible but awkward |
| Server config | Not designed for it | Excellent |
They're complementary, not competing. The standard pattern is: Terraform creates the VM and outputs its IP; Ansible connects to that IP and configures it.
Inventory
Ansible needs to know which machines to manage. An inventory is a list of hosts, optionally grouped:
# inventory.ini — static inventory
[webservers]
web1.example.com
web2.example.com ansible_user=ubuntu
[databases]
db1.example.com ansible_port=2222
[production:children]
webservers
databases
[production:vars]
ansible_python_interpreter=/usr/bin/python3
Dynamic inventories generate the host list at runtime — useful when Terraform manages your servers and IP addresses change. The AWS dynamic inventory plugin reads EC2 tags:
# aws_ec2.yaml — dynamic inventory
plugin: amazon.aws.aws_ec2
regions:
- us-east-1
filters:
tag:Environment: production
instance-state-name: running
keyed_groups:
- key: tags.Role
prefix: role
# Use it
ansible-inventory -i aws_ec2.yaml --list
ansible -i aws_ec2.yaml role_webserver -m ping
Playbooks
A playbook is a YAML file defining which tasks run on which hosts:
---
- name: Configure web servers
hosts: webservers
become: true # sudo
vars:
app_port: 8080
app_version: "2.1.0"
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Write nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/sites-available/app
mode: "0644"
notify: Reload nginx
- name: Enable site
ansible.builtin.file:
src: /etc/nginx/sites-available/app
dest: /etc/nginx/sites-enabled/app
state: link
- name: Start nginx
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Key playbook concepts:
- Tasks — individual actions using modules. Each task is idempotent: "ensure nginx is installed" won't reinstall if it's already there.
- Handlers — tasks triggered only when notified. The "Reload nginx" handler runs once at the end if any task notified it — even if 5 tasks triggered the notification.
- Templates — Jinja2 templates processed on the control node and copied to targets. Access variables with
{{ variable_name }}. - become — privilege escalation (sudo). Needed for most system-level operations.
Run a playbook:
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml --check # dry run
ansible-playbook -i inventory.ini site.yml --diff # show file diffs
ansible-playbook -i inventory.ini site.yml --limit webservers # subset of hosts
Roles
Roles are Ansible's reusability unit — a structured directory that bundles tasks, templates, variables, and handlers into a reusable component:
roles/
nginx/
tasks/
main.yml # tasks entry point
templates/
nginx.conf.j2 # Jinja2 templates
handlers/
main.yml # handlers
defaults/
main.yml # default variable values
vars/
main.yml # role-internal variables (higher priority than defaults)
meta/
main.yml # role dependencies
Use a role in a playbook:
---
- name: Configure production servers
hosts: production
become: true
roles:
- role: nginx
vars:
nginx_worker_processes: 4
- role: app_deploy
vars:
app_version: "{{ lookup('env', 'APP_VERSION') }}"
Share and consume roles via Ansible Galaxy:
# requirements.yml
roles:
- name: geerlingguy.docker
version: 7.1.0
- name: geerlingguy.nodejs
version: 6.1.0
collections:
- name: community.docker
version: ">=3.0"
ansible-galaxy install -r requirements.yml
Variables & Secrets
Variable precedence in Ansible (higher overrides lower):
- Extra vars (
-eflag) — highest - Task vars
- Block vars
- Role and include vars
- Play vars
- Host vars
- Group vars
- Role defaults — lowest
Ansible Vault encrypts sensitive values at rest:
# Encrypt a file
ansible-vault encrypt group_vars/production/secrets.yml
# Reference encrypted vars normally in playbooks — Ansible decrypts at runtime
# Decrypt at runtime
ansible-playbook site.yml --vault-password-file ~/.vault_pass
ansible-playbook site.yml --ask-vault-pass
---
# group_vars/production/secrets.yml (encrypted with Vault)
db_password: "s3cr3t-production-password"
api_key: "sk-prod-xxxxxxxxxxxxxxxx"
Pairing with Terraform
The standard integration pattern: Terraform outputs the newly created server IPs, then Ansible runs against them.
Option 1: Shell script orchestration
# deploy.sh
set -e
# Provision infrastructure
cd infra/
terraform init
terraform apply -auto-approve
# Get outputs
WEB_IP=$(terraform output -raw web_server_ip)
DB_IP=$(terraform output -raw db_server_ip)
# Configure servers
cd ../ansible/
cat > inventory.ini <
Option 2: Terraform local-exec provisioner
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
key_name = aws_key_pair.deploy.key_name
provisioner "local-exec" {
command = <<-EOT
sleep 30 # wait for SSH to be available
ansible-playbook \
-i '${self.public_ip},' \
-u ubuntu \
--private-key ${var.ssh_key_path} \
ansible/site.yml
EOT
}
}
Terraform provisioners run only at resource creation time and are skipped on subsequent terraform apply runs. They also complicate the apply lifecycle — if the provisioner fails, Terraform marks the resource as tainted and will destroy/recreate it on the next run. Shell script or CI/CD orchestration is more predictable.
Option 3: Dynamic inventory from Terraform state
The terraform-inventory tool generates an Ansible inventory directly from Terraform state:
# Install terraform-inventory
go install github.com/adammck/terraform-inventory@latest
# Use it directly
ansible-playbook -i $(which terraform-inventory) site.yml
Key Takeaways
- Ansible configures what runs on servers; Terraform provisions the servers. They solve different problems and work best together.
- Ansible is agentless — it connects over SSH. No daemon to install on managed nodes, only Python and SSH required.
- Playbooks are ordered YAML task lists. Tasks are idempotent by design — running a playbook twice converges to the same state as running it once.
- Handlers run at the end of a play, only once, only if notified. Use them for service restarts and reloads triggered by config changes.
- Roles are Ansible's reusability unit. Share them via Ansible Galaxy. Use
requirements.ymlto pin versions. - Ansible Vault encrypts secrets at rest in YAML files. Decrypt at runtime with a vault password file or environment variable.
- The cleanest Terraform + Ansible integration is external orchestration: Terraform outputs IPs, a shell script or CI job feeds them into an Ansible inventory, Ansible configures the servers.