Skip to content

Configuration Management (Ansible)

State & Drift covered how Terraform creates and tracks infrastructure. But creating a VM is only half the job. An empty VM doesn’t serve traffic — someone has to install the web server, write its config file, create users, set kernel parameters, and start the service. That second job is configuration management, and Ansible is its most widely used tool.

The distinction is the whole point of this page, so make it sharp:

PROVISIONING (Terraform) CONFIGURING (Ansible)
────────────────────────── ──────────────────────────────
creates the infrastructure sets up what runs INSIDE it
VMs, networks, load balancers, packages, config files, users,
disks, DNS, IAM roles services, OS settings
"make a server exist" "make this server run nginx,
configured this way, started"

Provisioning brings infrastructure into existence — the VM, the network, the disk. Configuring brings the software and settings on a machine to a desired state — install nginx, drop in its config, ensure it’s running. Terraform is built for the first; Ansible for the second. They’re complementary layers, not competitors: Terraform makes the box, Ansible furnishes it. (Plenty of teams use Terraform to provision and Ansible to configure, in that order.)

Ansible’s central design property is idempotence — the same property that made declarative IaC robust. You describe the desired state of a machine in a playbook (a YAML file of tasks), and each task is written so that running it repeatedly is safe: if the machine is already in the desired state, the task does nothing.

- name: Configure web servers
hosts: webservers
become: true # run with sudo
tasks:
- name: Ensure nginx is installed
ansible.builtin.apt:
name: nginx
state: present # "present", not "install" — a desired state
- name: Deploy the nginx config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
- name: Ensure nginx is running and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted

Read the task names: Ensure nginx is installed, Ensure nginx is running. That phrasing is deliberate. state: present doesn’t mean “run the install command” — it means “make it so this package is present; if it already is, do nothing.” Run this playbook on a fresh box and it installs and configures everything. Run it again and Ansible reports ok for every already-satisfied task and changed only for what it actually touched. You can run it daily to correct drift on the software layer, the same way terraform plan/apply corrects drift on the infrastructure layer.

The notify/handlers pattern adds a nice touch: nginx only restarts if its config actually changed — no needless restarts when nothing’s different.

A playbook says what to configure; an inventory says where — the list of target hosts, grouped by role. The hosts: webservers line above refers to a group defined here:

[webservers]
web1.example.com
web2.example.com
[dbservers]
db1.example.com

Group your hosts (webservers, dbservers) and you can target a playbook at exactly the machines it applies to. This is also a natural seam with Terraform: Terraform creates the servers and can output their IPs, which become the Ansible inventory — provisioning feeds configuring.

Ansible’s operating model contrasts sharply with the pull-based reconciler you saw in Kubernetes. Ansible is push-based and agentless:

PUSH (Ansible) PULL (Kubernetes / GitOps)
───────────────────────── ─────────────────────────────
you run a command from a an agent on each node
control machine continuously fetches desired
│ state and converges
├─ SSH ─► web1 ┌──────┐
├─ SSH ─► web2 │ node │◄─ pulls from
└─ SSH ─► db1 └──────┘ control plane
nothing installed on targets agent runs on every target
converges WHEN you push converges CONTINUOUSLY

You run ansible-playbook from a control machine; Ansible connects to each host over plain SSH, pushes the configuration, and converges it — at that moment. There’s no agent or daemon to install on the targets (a real operational win: nothing extra to secure or keep alive). The trade-off is that convergence happens when you push, not continuously. If something drifts an hour after your run, Ansible won’t notice until the next push — unlike a Kubernetes controller, which is always watching.

Terminal window
# Push the configuration to every host in the inventory
ansible-playbook -i inventory.ini site.yml
# Preview changes without applying them (Ansible's dry run)
ansible-playbook -i inventory.ini site.yml --check
TerraformAnsible
Primary jobProvision infrastructureConfigure machines
LayerThe box, network, cloudThe OS, packages, services inside
ModelDeclarative + reconcilerProcedural tasks, idempotent
StateExplicit state fileNo persistent state; checks reality each run
ExecutionComputes a graph, appliesRuns tasks top-to-bottom over SSH
Driftplan diffs against stateRe-run the playbook to re-converge

Ansible runs tasks in order (more procedural than Terraform’s graph), and it keeps no state file — each task checks the live machine and acts only if needed. Different tools, same underlying philosophy: declare the desired state, make running it repeatedly safe.

But notice the recurring weakness: both Terraform and Ansible correct drift after the fact. The next page asks the deeper question — what if a server simply never changed after birth, so drift had nowhere to creep in? That’s immutable infrastructure.

Five questions to place configuration management against its neighbors:

  • Why does it exist? Because provisioning a VM is only half the job — something has to install nginx, write its config, and start the service inside the box, and a hand-run bash script over SSH is imperative and brittle.
  • What problem does it solve? Servers hand-configured differently: idempotent playbooks (state: present, not “run the install command”) bring every host in a group to the same declared state, and re-running drags a drifted box back to spec.
  • What are the trade-offs? It’s push-based and agentless (nothing to install or secure on the targets), but convergence happens only when you push, so drift an hour later goes unnoticed until the next run — and idempotence is the module’s promise, which raw shell/command tasks leak unless you add creates:/changed_when: guards.
  • When should I avoid it? When immutable infrastructure fits — bake a golden image and there’s no live server to configure — or when a continuously-watching pull reconciler (Kubernetes) is the better model.
  • What breaks if I remove it? Server configuration reverts to hand-editing over SSH: every box subtly different, impossible to reproduce, with no single source of truth to re-converge against.
  1. Draw the line between provisioning and configuring. Which tool owns each, and why are they complementary rather than competing?
  2. Why is a plain bash setup script a poor way to configure a server? What property do configuration management tools add to fix it?
  3. The task state: present is phrased as a desired state, not a command. Explain what that buys you when the playbook runs a second time.
  4. What is an inventory, and how does it connect naturally to the output of a Terraform run?
  5. Contrast Ansible’s push-based, agentless model with Kubernetes’ pull-based reconciler. What is the key trade-off in when each converges?
Show answers
  1. Provisioning brings infrastructure into existence (VMs, networks, disks, DNS) — Terraform’s job; configuring brings the software and settings inside a machine to a desired state (packages, config files, services) — Ansible’s job. They’re complementary layers, not competitors: Terraform makes the box, Ansible furnishes it.
  2. A bash script is imperative and brittle: run it twice and it may create the user twice, append the config line twice, or error because the package is already installed. Configuration management tools add idempotence — each task is “make this so; if it already is, do nothing” — so re-running is safe.
  3. state: present means “ensure this package is present; if it already is, do nothing” rather than “run the install command.” On a second run that buys you safety and drift correction: already-satisfied tasks report ok and only genuinely-needed changes report changed, so re-running drags a drifted box back to spec.
  4. An inventory is the list of target hosts, grouped by role (e.g. [webservers]), telling a playbook where to run. It connects to Terraform naturally because Terraform creates the servers and can output their IPs, which become the Ansible inventory — provisioning feeds configuring.
  5. Ansible is push-based and agentless: you run ansible-playbook from a control machine, it SSHes to each host and converges it at that moment, with nothing installed on the targets. A Kubernetes reconciler is pull-based: an agent on each node continuously fetches desired state and converges. The trade-off is when they converge — Ansible only when you push (drift an hour later goes unnoticed until the next run), the reconciler continuously.