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.
Provisioning vs configuring
Section titled “Provisioning vs configuring”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.)
Idempotent playbooks
Section titled “Idempotent playbooks”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: restartedRead 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.
Inventories: which machines
Section titled “Inventories: which machines”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.comweb2.example.com
[dbservers]db1.example.comGroup 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.
Push-based, agentless
Section titled “Push-based, agentless”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 CONTINUOUSLYYou 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.
# Push the configuration to every host in the inventoryansible-playbook -i inventory.ini site.yml
# Preview changes without applying them (Ansible's dry run)ansible-playbook -i inventory.ini site.yml --checkHow it differs from Terraform
Section titled “How it differs from Terraform”| Terraform | Ansible | |
|---|---|---|
| Primary job | Provision infrastructure | Configure machines |
| Layer | The box, network, cloud | The OS, packages, services inside |
| Model | Declarative + reconciler | Procedural tasks, idempotent |
| State | Explicit state file | No persistent state; checks reality each run |
| Execution | Computes a graph, applies | Runs tasks top-to-bottom over SSH |
| Drift | plan diffs against state | Re-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.
The architect’s lens
Section titled “The architect’s lens”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/commandtasks leak unless you addcreates:/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.
Check your understanding
Section titled “Check your understanding”- Draw the line between provisioning and configuring. Which tool owns each, and why are they complementary rather than competing?
- Why is a plain bash setup script a poor way to configure a server? What property do configuration management tools add to fix it?
- The task
state: presentis phrased as a desired state, not a command. Explain what that buys you when the playbook runs a second time. - What is an inventory, and how does it connect naturally to the output of a Terraform run?
- Contrast Ansible’s push-based, agentless model with Kubernetes’ pull-based reconciler. What is the key trade-off in when each converges?
Show answers
- 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.
- 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.
state: presentmeans “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 reportokand only genuinely-needed changes reportchanged, so re-running drags a drifted box back to spec.- 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. - Ansible is push-based and agentless: you run
ansible-playbookfrom 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.