Lesson 3 of 9 · 24m
Playbooks and the Idempotency Contract
What a play actually is, why modules beat shell commands, and how changed/ok/failed tell you whether your automation is honest.
The senior-engineer Ansible smell test: run the playbook twice. Second run should be all green — ok, no changed. If tasks report changed on a converged system, the playbook is lying about idempotency, and one day that lie becomes "we restarted nginx 400 times in a rolling deploy" or "the same cron line got appended 40 times."
Anatomy: plays map hosts to tasks
# playbooks/web.yml
- name: Configure web tier
hosts: webservers
become: true
tasks:
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
update_cache: true
- name: Ship site config
ansible.builtin.template:
src: nginx-site.conf.j2
dest: /etc/nginx/sites-available/acme.conf
mode: "0644"
notify: Reload nginx
handlers:
- name: Reload nginx
ansible.builtin.service:
name: nginx
state: reloadedA play binds hosts: to a list of tasks:. A playbook is a list of plays — web.yml above might be followed by a Configure db tier play in the same file. Handlers run once at the end, only if something notified them.
Modules vs. command: the honesty problem
command/shell run a string and can't know if it changed anything, so they report changed every time — which destroys the signal that makes Ansible useful. Modules inspect, then act:
# dishonest — reports changed every run, appends duplicates
- name: Add repo
ansible.builtin.shell: echo "deb ..." >> /etc/apt/sources.list
# honest — checks state, only changes when needed
- name: Add repo
ansible.builtin.apt_repository:
repo: "deb https://apt.postgresql.org/pub/repos/apt jammy-pgdg main"
state: presentIf you must use command, restore honesty manually: creates: / removes: args, or changed_when/failed_when on a registered result. A task that reports ok when it did nothing and changed when it did something is telling the truth; that's the whole contract.
Check mode and diff: read-only rehearsal
ansible-playbook web.yml --check --diff--check asks modules "would you change anything?" without doing it — apt supports it, command is skipped by default. --diff shows the before/after for file changes. Run check+diff in CI on every PR and "what does this playbook do?" becomes a diff review.
Field notes
name:on every task. The output line is your run log; "TASK [apt]" tells an on-call engineer nothing at 3 a.m.- FQCN names (
ansible.builtin.apt) survived collections; bareapt:still works but lint will flag it. - Idempotency is per-attribute:
state: presentinstalls nginx but won't upgrade it —latestis a different promise. - Execution order bites: tasks run top-to-bottom, handlers fire at play end (or on
meta: flush_handlers), and a failed host drops out of that play while others continue —any_errors_fatal/serialexist for when that default is wrong.