unit 4 · Playbooks
Playbook anatomy
One file, three nouns deep
you may want to read 10 · Patterns, --limit & per-env inventories first — but nothing is locked
tl;dr
- A playbook is a list of plays; a play targets hosts and holds tasks; each task calls one module.
- hosts: takes a lesson-10 pattern; -i picks the inventory that resolves it — same playbook, any environment.
- Read the output per task: ok means already in the desired state, changed means Ansible altered something.
why it matters Playbooks are the unit of real Ansible work — until you can read plays, tasks and the ok/changed output, every repo at your job is just a wall of YAML.
Everything so far named one module and one pattern, typed fresh. Ad-hoc commands are for checks and one-offs. Repeatable work gets written down in a playbook — a YAML file that is a list of plays.
Three nouns, strictly nested, carry the whole design. A playbook is a list of play s. A play targets hosts and holds tasks. A task calls exactly one module. That is the entire grammar. Everything ahead in this unit and the next (variables, conditionals, loops, handlers) hangs off it without changing its shape.
The file
- name: Configure web servers # a PLAY — first item in the playbook's list
hosts: web # a lesson-10 pattern, resolved against -i
become: true # escalate privileges for every task below
gather_facts: true # survey each host before the first task
tasks:
- name: Push the MOTD file # a TASK — one module call
ansible.builtin.copy:
src: files/motd
dest: /etc/motd
- name: Ensure nginx is running
ansible.builtin.service:
name: nginx
state: startedThe leading - matters: the file’s top level is a YAML list,
and this play is its only item. Append a second - name: block targeting
db and you have a two-play playbook — one file rolling changes across an
estate in order, like a repo’s site.yml.
Five keywords make the play:
name:— free text, echoed in the run output. Name every play and task; unnamed tasks make output unreadable at 3 a.m.hosts:— a pattern from lesson 10:web,web:&staging,all. It names no machines. It resolves at run time against the inventory you pass.become: true— privilege escalation for every task in the play (root via sudo on Linux; Windows does this differently, unit 8). Old content writessudo:— that keyword is long gone.gather_facts: true— Ansible surveys each host (OS, IPs, memory) before the first task. It defaults to true; it’s written here so the extra output step below doesn’t look like magic.tasks:— the ordered module calls. Each one: a name, one FQCN module, arguments as a YAML dict. Thekey=valueshorthand from ad-hoc commands stays on the command line — in playbooks it fails lint.
Running it, reading it
ansible-playbook site.yml -i inventories/staging/PLAY [Configure web servers] ***********************
TASK [Gathering Facts] *****************************
ok: [web01]
TASK [Push the MOTD file] **************************
changed: [web01]
TASK [Ensure nginx is running] *********************
ok: [web01]
PLAY RECAP *****************************************
web01 : ok=3 changed=1 unreachable=0 failed=0ok — the host was already in the state the
task describes; the module touched nothing. changed — the module
altered the machine. Here nginx was already running, but /etc/motd
differed, so copy rewrote it. The PLAY RECAP totals those per host.
Read it first, every run.
Run it again, changing nothing: every task reports ok, recap says
changed=0. Modules check current state before acting — that is
idempotent behaviour, and why re-running a playbook is
safe. A playbook that reports changed on identical re-runs is broken,
and colleagues will say so.
Notice what this file does not have. Values are hardcoded; variables fix that (lesson 16). Every task runs on every host; conditionals add branching (lesson 18) and loops add repetition (lesson 19). Nothing reacts when the copy changes a config; restart-on-change is handlers (lesson 20). At scale, plays don’t carry task lists inline — they call roles (lesson 24). All of it slots into this skeleton, which never changes.
replay — site.yml against web01 — run it, then run it again
a recording, not a simulator — tap ⓘ lines for what they mean
at work, this sounds like
- “It’s not idempotent.”
- Re-running the playbook keeps reporting changed= when nothing should change — a clean second run ends with changed=0.
self-check — recall, not a test
1 Playbook, play, task — how do the three nest, and what does each one do?
2 What decides which actual machines "hosts: web" touches?
3 You run an unchanged playbook a second time. What should the recap show, and what would changed=2 tell you?
lab (optional — never required)
Copy site.yml, set hosts: all, aim the copy task at /tmp/motd, drop the service task and become, then run it twice against a localhost inventory and compare changed= across the two recaps.
needs: The control node from lesson 5.