Skip to main content

Configuration Management: Ansible and the Line with IaC

What This Concept Is

Two different problems that get conflated:

  • Infrastructure as Code (Terraform, CloudFormation, Pulumi, CDK) -- provision the boxes: servers, VPCs, databases, DNS, IAM.
  • Configuration management (Ansible, Chef, Puppet, Salt) -- configure what is inside the boxes: packages, files, users, services.

Ansible is the dominant modern config-management tool. It uses SSH (or WinRM, or direct API plugins) to push a desired state onto a target system, driven by YAML playbooks.

An Ansible playbook is declarative about task outcomes (idempotent module calls like apt, file, service) but ordered -- tasks run top-to-bottom. It has no native equivalent of a state file; it refreshes facts each run and relies on modules to be idempotent.

Why It Matters Here

Teams often reach for Terraform to install nginx on an EC2 instance and wonder why it feels awkward. The awkwardness is the line. Terraform created the instance; nginx lives inside it. Making Terraform SSH in to install nginx is pushing the tool across a boundary it was not designed for.

The modern answer is often "neither, use an AMI baked with Packer" or "use a container image." But Ansible is still the right tool when:

  • you operate long-lived VMs (on-prem, regulated environments, legacy Windows)
  • you have to apply changes across hundreds of existing hosts without rebuilding them
  • you have drift inside the box (package upgrades, user changes, policy updates)

Concrete Example

A tiny Ansible playbook that installs and configures nginx:

---
- name: Configure web tier
hosts: web
become: true
vars:
nginx_port: 80
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true

- name: Deploy nginx config
ansible.builtin.template:
src: templates/nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
mode: "0644"
notify: Reload nginx

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

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

Observations:

  • Each task is idempotent at the module level (apt state=present, template, service state=started).
  • Playbook order matters: the config must be deployed before the service tries to reload.
  • become: true runs with sudo privileges.
  • The inventory (not shown) is a separate file listing which hosts are in the web group. Ansible has no state file; the inventory is the closest analog.

The Line, Drawn

TaskIaC (Terraform)Config management (Ansible)
Provision EC2 instanceYesNo
Create DNS recordYesNo
Install nginx on the instanceUsually noYes
Configure /etc/nginx/nginx.confNoYes
Create S3 bucketYesNo
Rotate user passwords on 200 serversNoYes
Build an AMI / container imageUse Packer, not eitherUse Packer or Dockerfile
Roll a Kubernetes DeploymentYes (K8s provider)No
Bootstrap a one-off VMMinimal Terraform + user_dataAnsible

Modern pattern: Terraform creates the box; the box is immutable (baked by Packer from an Ansible playbook, or built from a container image). No in-place configuration at all. Change = replace.

Legacy pattern: Terraform creates the box; Ansible configures it; both run as part of the same pipeline.

Anti-pattern: Terraform creates the box and also SSHes in via remote-exec to install things. The provisioner has no idempotency guarantee and breaks every assumption about plan/apply.

Common Confusion / Misconception

"Ansible is IaC." It can manage some infrastructure (via cloud modules), but its strength is inside-the-box configuration. Using Ansible to manage VPCs makes you long for terraform plan by week two.

"Terraform can replace Ansible because it has provisioners." It cannot. Provisioners in Terraform are a documented escape hatch, not a feature. HashiCorp's own guidance calls them a last resort.

"Immutable infrastructure makes Ansible obsolete." For cloud-native workloads, largely yes -- images are built once and replaced on change. For long-lived VMs or heterogeneous fleets, Ansible still pays rent.

"An Ansible playbook run twice is safe because modules are idempotent." Module-level idempotency is local. Playbook-level idempotency requires that every task (including command and shell) is idempotent -- which is on you.

How To Use It

  1. Draw the line explicitly in your architecture: "Terraform manages [X list]. Ansible manages [Y list]. Packer builds images from [Z playbooks]."
  2. Prefer immutable infrastructure for cloud-native workloads. Use Ansible to build images, not to run against live production boxes.
  3. If you must run Ansible against live hosts, put it in a pipeline, not on a laptop. Source of truth is the playbook, not the engineer running it.
  4. Do not use Terraform local-exec/remote-exec to do Ansible's job. It creates a workflow nobody can reason about.

Check Yourself

  1. Why is "Terraform + remote-exec + install-nginx.sh" fragile?
  2. What is the difference between Ansible's inventory and Terraform's state file?
  3. Give one task you would use Ansible for today, and one you would use Terraform for, on the same server.

Mini Drill or Application

Sketch a pipeline in 15 minutes:

  • Packer builds an AMI using an Ansible playbook that installs a web server and deploys your app artifact.
  • Terraform creates an ASG that uses that AMI, plus a load balancer.
  • Deploys are "bake new AMI -> Terraform updates the launch template -> ASG rolls."

Identify where config drift becomes impossible in this design (and where it is merely mitigated). This is the logic behind immutable infrastructure.

See also (external)


Source Backbone

Infrastructure-as-code details are tool-specific, but these local books provide the operational backbone for shell, Git, and change discipline.