Ansible
Configuration management. Less central than it was — containers absorbed much of its job — but still standard for VM fleets, on-prem, and anything Terraform provisions but doesn’t configure.
The model
Agentless: Ansible connects over SSH, pushes modules, runs them, removes them. Nothing to install on targets beyond Python.
# playbook.yml
- hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
- name: Deploy config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s # verify BEFORE replacing
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
The validate parameter is the detail worth knowing: it checks the rendered config before installing it, so a bad template fails the task instead of breaking the service on reload.
Handlers run once at the end, only if notified. That’s how you avoid restarting a service five times in one playbook run.
Terraform vs Ansible
The comparison you’ll be asked for:
| Terraform | Ansible | |
|---|---|---|
| Job | provision infrastructure | configure what exists |
| Model | declarative, with state | procedural tasks, idempotent modules |
| State file | yes — tracks reality | no |
| Drift detection | plan shows it |
only if you re-run |
| Destroys things | yes, deliberately | rarely |
| Best at | cloud resources, networks, managed services | packages, files, services, app deploys |
They compose: Terraform creates the VMs and outputs their IPs; Ansible configures them. Using Terraform to install packages via remote-exec, or Ansible to manage cloud resources, is possible and worse at both.
The stronger 2026 answer: if you’re building images or containers, you often need neither for configuration. Packer or a Dockerfile bakes the configuration in, and instances become immutable. Ansible earns its place where you have long-lived mutable servers.
Idempotency
The property that makes re-running safe. Modules declare desired state, not commands:
# Idempotent - describes the end state
- ansible.builtin.package: { name: nginx, state: present }
# NOT idempotent - runs every time, reports "changed" every time
- ansible.builtin.shell: apt-get install -y nginx
shell and command are escape hatches that break idempotency. When you must use them, guard with creates:, removes: or a when: condition, and set changed_when: honestly so your change reporting means something.
A playbook that reports changes on every run is a playbook nobody can use to detect drift.
Structure at scale
inventory/
production/hosts.yml
staging/hosts.yml
roles/
nginx/{tasks,handlers,templates,defaults,vars}/
group_vars/
webservers.yml
host_vars/
web-01.yml
Roles are the reuse unit. Inventory separates environments. Variable precedence is deep and a genuine source of confusion — roughly: role defaults lose to group vars, which lose to host vars, which lose to -e extra vars. When a variable “isn’t taking effect”, precedence is the first thing to check.
Secrets
ansible-vault encrypt group_vars/production/secrets.yml
ansible-playbook site.yml --ask-vault-pass
Ansible Vault encrypts files at rest in the repo. It works, and it’s inferior to a real secrets manager: rotation is manual, access is all-or-nothing per file, and there’s no audit trail. Prefer pulling from Vault, AWS Secrets Manager or similar at runtime; use Ansible Vault when you have no other option. See ../../system_design/04_secrets_config/01_secrets_and_configuration.md.
Practical notes
--check --diffis your dry run. Not all modules support it faithfully, so it’s indicative rather than authoritative.--limitto target a subset; combine withserial:for rolling deploys across a fleet.- Performance: Ansible is slow on large inventories. Increase
forks, enable pipelining, and usegather_facts: falsewhere you don’t need facts — fact gathering is often the single biggest cost. - Testing: Molecule spins up containers and verifies roles. Rare in practice, valuable when playbooks are load-bearing.
Where it fits in 2026
| Use it for | Prefer something else |
|---|---|
| long-lived VMs and on-prem fleets | containerised workloads (image build) |
| network devices, appliances | cloud resources (Terraform) |
| ad-hoc fleet operations | ephemeral autoscaled instances (bake the image) |
| bootstrapping Kubernetes nodes | app config in K8s (ConfigMaps, Helm) |
Being clear that immutable infrastructure has narrowed Ansible’s scope is a better answer than presenting it as the default.
Interview angle
- “Terraform or Ansible?” — different jobs. Terraform provisions infrastructure and holds state so it can detect drift and destroy; Ansible configures existing machines and holds no state. They compose: Terraform creates the VMs, Ansible configures them.
- “What makes a playbook idempotent, and how does it break?” — modules declare desired state, so re-running is a no-op.
shellandcommandbreak it because they always execute; guard them withcreates:/when:and setchanged_when:honestly, or your change reporting becomes meaningless. - “Why is Ansible less central than it used to be?” — immutable infrastructure. If you bake configuration into an image or container, there’s nothing to configure at runtime. Ansible earns its place with long-lived mutable servers, network devices and on-prem fleets.
- “A variable isn’t taking effect. Where do you look?” — precedence. Role defaults are weakest, then group vars, then host vars, then extra vars on the command line. It’s the most common source of confusion in a large inventory.
- “Is Ansible Vault adequate for secrets?” — workable, not good. No rotation, no per-secret access control, no audit trail. Prefer fetching from a real secrets manager at runtime.
- “Your playbook takes 20 minutes across 200 hosts. Speed it up?” — raise
forks, enable pipelining, and disable fact gathering where it isn’t needed; fact gathering is usually the dominant cost.