AnsibleXI · PlaybooksPlaybooks
A playbook someone else can operate
What you'll learn
- Judge a playbook by whether a colleague could operate it at 03:00
- Recognise the structural smells that predict an unoperable playbook
- Decide when a task list has earned promotion to a role
- Keep environment-specific values out of the playbook and in inventory
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
The test for a playbook is not whether it works. It is whether the person on call — who did not write it, has not read it, and is looking at it for the first time during an incident — can decide in two minutes whether running it is safe.
That is a different property from correctness, and it is the one that degrades silently.
What “operable” means, concretely
A colleague opens your playbook at 03:00. They need five answers, fast:
- Which hosts? Answered by the
hosts:line of each play, in isolation, without cross-referencing anything. - What will it change? Answered by the task names, read in order.
- Is it safe to run twice? Answered by whether the tasks are
declarative module calls or
shelllines. - What does it need that might be missing? Answered by whether required variables are declared and asserted, or assumed.
- What happens if it stops halfway? Answered by whether restore
steps are in
post_tasks(which a failed host skips) or in a block’salways(which it does not).
Every structural rule below exists to keep one of those five answers readable.
One purpose per play
A play should do one thing to one class of machine. When you find
yourself writing “and” in the play name — Deploy the app and rotate the certificates and clean up old releases — you have three plays, or
one play that three different people will want to run separately.
The cost of merging them is paid at 03:00, when someone needs only the certificate rotation and the only way to get it is to run all three or to copy tasks into a new file during an incident.
Keep logic out of the file
A playbook describes what should be true. The moment it starts deciding which truth applies, based on branching, it has become a program in a language with no functions, no tests and no debugger.
The specific smells:
| Smell | What it usually means | Where the logic belongs |
|---|---|---|
| Conditionals stacked four deep | The play targets too broad a group | The hosts: line, or group membership in inventory |
when: inventory_hostname == 'web03.example.com' | A host-specific exception | host_vars/web03.example.com.yml |
A set_fact chain deriving a value from three others | A computed configuration value | A group var, or a role default |
| Long inline Jinja with nested filters | A rendering decision | A template file, or a filter with a name |
A 400-line tasks: block | Several roles that have not been extracted | Roles |
A play targeting all with per-task guards | Scope expressed as filtering | Separate plays per target |
Ansible’s conditional syntax works perfectly well. The objection is not
that when: is bad — it is that a decision encoded in a conditional is
invisible to --list-hosts, invisible to the inventory, and visible
only to someone reading every line of the file.
Declare what you require
A playbook that needs app_version and does not say so fails at task 9
with a Jinja error naming a template. A playbook that asserts its
inputs fails at task 1 with a sentence a human wrote:
pre_tasks:
- name: Require the variables this play cannot run without
ansible.builtin.assert:
that:
- app_version is defined
- db_host is defined
- deploy_user is defined
fail_msg: >-
Set app_version, db_host and deploy_user in group_vars for this
environment. See docs/deploy.md.
This costs four lines and one task. It converts every “why did this fail on staging” conversation into a message that answers itself. The guardrails part builds this into a full precondition discipline; the habit starts here.
When a task list should become a role
Not every task list needs to be a role. Premature extraction produces a
roles/ directory full of single-use roles with one variable each,
which is its own maintenance problem.
The signals that a task list has earned promotion:
- It is used by more than one playbook. The strongest signal, and the only one that is purely objective.
- It has its own variables with sensible defaults. A role’s
defaults/main.ymlis a real interface; a task list has no way to declare one. - It has files and templates. Roles resolve
files/andtemplates/relative to themselves, which a bare task list cannot do cleanly. - It is long enough that you scroll past it. Roughly 40–50 tasks, though the real test is whether you have started using comments as section headers.
- Someone else would plausibly want it. Not “would publish it to Galaxy” — just “the database team would use this”.
The signals that it should stay a task list:
- It is used once, by one playbook, and always will be.
- It is glue: three tasks that connect two roles.
- Extracting it would mean inventing five variables to parameterise things nobody varies.
Reading a playbook the way a reviewer should
The review order is not top to bottom. It is:
- Every
hosts:line. How much of the estate is in scope? - Every
become:. What runs as root, and does it need to? --list-tasks. Does the resolved order match the intent?- Every
shellandcommandtask. Are they fenced withcreatesor given accurate change reporting? - The variables. Are environment-specific values in inventory, or baked in?
- The restore path. If this stops halfway, what is left in a partial state, and does anything undo it?
Only then read the tasks in order.
$ ansible-playbook -i inventory.ini site.yml --list-tasksplaybook: site.yml
play #1 (lb): Configure the load balancer TAGS: [lb]
tasks:
Render the backend pool TAGS: [config, lb]
play #2 (web): Deploy the web tier TAGS: []
tasks:
Take the host out of the load balancer pool TAGS: []
webserver : R1 role task, notifies H-role TAGS: []
Apply the site-specific tuning TAGS: []
Return the host to the load balancer pool TAGS: []
play #3 (db): Configure the database tier TAGS: [db]
tasks:
Check replication lag TAGS: [db, health]Knowledge check
Knowledge check · 4 questions
Q1. A play named Deploy the app, rotate certificates and clean up old releases works correctly. What is the strongest argument for splitting it into three plays?
Q2. Where should the value of db_host live for a playbook that runs against both staging and production?
Q3. Which are genuine signals that a task list has earned promotion to a role? Select all that apply.
Q4. A pre_tasks assertion that the required variables are defined turns a late, confusing failure into an early, self-explanatory one.
Passing score: 75%. Answers are checked in this browser.