Ansible
Bringing servers to a described state over SSH — no agent, no bootstrap problem, and safe to run twice.
Terraform creates the machine. Something still has to install the packages, write the config, and start the service — and if that something is a shell script somebody ran once in 2023, nobody can tell you what’s on the box.
Ansible describes the desired state of a machine in YAML and pushes it over SSH. No agent to install, which is why it survives in the places that matter: legacy fleets, appliances, network gear, and anywhere containers never arrived.
Install and check connectivity
pipx install --include-deps ansible # or: brew install ansible
inventory.ini — the machines:
[web]
web1.example.com
web2.example.com
[db]
db1.example.com ansible_user=deploy
ansible -i inventory.ini all -m ping
If that returns green, everything else is detail.
A playbook
- name: Configure web servers
hosts: web
become: true
vars:
app_port: 8080
tasks:
- name: Install packages
ansible.builtin.package:
name: [nginx, git]
state: present
- name: Write the site config
ansible.builtin.template:
src: templates/site.conf.j2
dest: /etc/nginx/conf.d/site.conf
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
ansible-playbook -i inventory.ini site.yml --check --diff # dry run first
ansible-playbook -i inventory.ini site.yml
ansible-playbook -i inventory.ini site.yml --limit web1.example.com
Two ideas carry the whole tool. Idempotence: tasks describe a state, so running the playbook twice changes nothing the second time — that’s what makes it safe to run on a schedule. Handlers: the reload fires only if the template actually changed, so you don’t bounce nginx on every run.
The habits that keep it maintainable
--check --diffbefore every real run. It prints what would change. On a fleet you don’t know well, this is the difference between a change and an incident.- Roles, once a playbook passes about fifty lines.
ansible-galaxy init roles/webgives the standard layout, and roles are the unit you reuse and test. - Encrypt secrets with Vault.
ansible-vault encrypt group_vars/prod/secrets.ymlkeeps them in Git safely — see Secrets management for when to outgrow that. - Prefer modules to
command. Ashell:task is a script with none of the idempotence; when you must use one, give itcreates:orchanged_when:. - Lint it.
ansible-lintcatches the deprecated syntax and the unsafe patterns before your colleagues do.
Where it fits today
| Situation | Tool |
|---|---|
| Create cloud resources | Terraform |
| Configure an OS on a long-lived machine | Ansible |
| Ship an application repeatedly | A container image and Helm |
| One-off task across 200 hosts | ansible all -m ..., ad hoc |
The honest summary: if your workload is containers on Kubernetes, you may never need it. If you have servers — and most organisations do, somewhere — Ansible is how you stop those servers being folklore.
Next
The one thing that must never sit in a playbook in plaintext → Secrets management
Last updated 25 Aug 2026, 00:00 UTC.