Skip to main content
RunBook Academy

AnsibleXI · PlaybooksPlaybooks

The pre-flight options, and what each one proves

Intermediate⏱ ~18 minansible-playbook

What you'll learn

  • Run the pre-flight ladder in order and state what each rung establishes
  • Name the specific claim each option does not make
  • Recognise which tasks check mode silently skips rather than predicts
  • Use --start-at-task and --step for recovery and inspection

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

Not yet marked complete on this device.

The Linux course’s linux-safe-cm-rollout teaches the habit: never run configuration management against production without a ladder of increasingly expensive checks first. This lesson gives that ladder its precise Ansible form — and, more usefully, states what each rung does not prove.

An option that answers a narrower question than you think it does is worse than no option at all, because it produces confidence.

The ladder

RungOptionProvesDoes not prove
1--syntax-checkThe playbook and its statically included files parseThat any module, host, variable or path exists
2--list-hostsThe set of hosts each play resolves to, and its sizeThat those hosts are reachable, or in what order they run
3--list-tasksThe resolved execution order of the tasks you wroteThat the tasks will succeed, or what dynamic includes will add
4--list-tagsWhich tags exist, so --tags will select what you expectThat a tagged subset is a coherent unit of work
5--checkA prediction of what would change, per hostAnything about tasks that do not support check mode
6--check --diffThe predicted content of file changesThat the rendered content is correct, only that it differs
7--step / --start-at-taskInteractive or partial execution for recoveryThat skipping earlier tasks was safe

Climb it in order. Each rung is cheaper than the one below it and rules out a class of mistake that would otherwise waste the next rung’s time.

1. --syntax-check — it parses

Read-only / Safethe cheapest rung
$ ansible-playbook -i inventory.ini site.yml --syntax-check
playbook: site.yml

That single line and exit code 0 are the whole result. It confirms the YAML is valid, the play keys are recognised, and everything reachable through import_playbook and import_tasks also parses.

It does not open a connection, does not check that ansible.builtin.tempalte is a real module name, and does not evaluate a single Jinja expression. A playbook that passes --syntax-check can still fail on its first task on every host.

Run it in CI on every commit. It is fast, it catches the indentation mistakes that YAML makes easy, and it costs nothing.

2. --list-hosts — this is who

Covered in the previous lesson. The number in hosts (n) is your blast radius. Read it for every play in the file, not just the one you changed.

3. --list-tasks — this is what, and in what order

Read-only / Safethe resolved task order across a composed playbook
$ ansible-playbook -i inventory.ini site.yml --list-tasks
playbook: 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]

This is the rung that catches structural mistakes: a role you forgot you had included, tasks in an order you did not intend, a pre_tasks step that is not where you thought it was.

Its limits are specific and worth memorising:

  • Gathering Facts is absent. It is injected by the play, not written by you.
  • Handlers are absent. They are conditional and have no fixed position.
  • include_tasks and include_role contribute nothing. Dynamic includes are resolved at execution time, so their contents cannot appear in a static listing. import_tasks and import_role are static and do appear.

The last one is the trap. A playbook that is mostly include_role can produce a short, reassuring --list-tasks output for a run that will execute two hundred tasks.

4. --list-tags — what --tags will select

Read-only / Safetags available for selection
$ ansible-playbook -i inventory.ini site.yml --list-tags
playbook: site.yml

play #1 (lb): Configure the load balancer	TAGS: [lb]
    TASK TAGS: [config, lb]

play #2 (web): Deploy the web tier	TAGS: []
    TASK TAGS: []

play #3 (db): Configure the database tier	TAGS: [db]
    TASK TAGS: [db, health]

The load-balancer play declares tags: ['lb'] at play level and its one task declares tags: ['config']. The task’s effective tags are both. Play-level tags are inherited, which is how --tags lb selects the whole play.

What this does not prove is that --tags config selects a coherent subset. Tag selection is per task, and a tag that pulls in the task that writes a config file but not the handler that reloads the service produces a half-applied change. Tags are a filter, not a unit of work, and the error-handling part returns to this.

5. --check — a prediction, with holes in it

Check mode asks every module to report what it would do without doing it. Where a module implements check mode properly, the result is a genuine prediction, and the recap’s changed count is the number of tasks that would have changed something.

The hole is that not every module implements it, and the ones that do not are skipped:

Read-only / Safewhat check mode does to a command task
$ ansible-playbook -i inv2.ini checkmode.yml --check
TASK [A debug task] ************************************************************
ok: [localhost] => {
  "msg": "hello"
}

TASK [A command task with no check-mode support] *******************************
skipping: [localhost]

TASK [A stat probe] ************************************************************
ok: [localhost]

TASK [Report the probe] ********************************************************
ok: [localhost] => {
  "msg": "exists=True"
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

skipped=1. Not changed=1, not a warning, not an error — a skip that looks exactly like a when: condition evaluating false.

Check mode also cannot predict second-order effects. A task that templates a config file can tell you the file would change; it cannot tell you the service will fail to start with the new content.

6. --check --diff — the predicted content

Adding --diff makes file-writing modules print a unified diff of the change they would make. It converts “this file would change” into “these three lines would change”, which is the difference between a prediction you can review and one you can only count.

7. --start-at-task and --step — recovery and inspection

--start-at-task skips forward to the first task whose name matches and begins there:

Configuration changeresume a play partway through
$ ansible-playbook -i inv2.ini checkmode.yml --start-at-task='A stat probe'
TASK [A stat probe] ************************************************************
ok: [localhost]

TASK [Report the probe] ********************************************************
ok: [localhost] => {
  "msg": "exists=True"
}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

This is the fastest way to resume a long play that failed at task 40, and it is also a good way to break one. The skipped tasks did not run, so anything they registered is undefined and anything they set with set_fact is missing. If task 12 registered a value that task 45 reads, starting at task 40 gives you an undefined-variable failure — or worse, a default that silently differs.

Matching is on the first task whose name matches, which is the second reason task names should be unique.

--step prompts before each task, letting you walk a play by hand. On 2.21.3 the prompt is:

Perform task: TASK: A debug task (N)o/(y)es/(c)ontinue:

Note the capitalised N — the default is no. Pressing return at every prompt on 2.21.3 executed nothing and produced an empty PLAY RECAP with no host line at all: a safe default, and also one that lets you walk past a step you meant to take without noticing. It is an inspection tool for a playbook you do not trust and a demonstration tool for a review; it is not something to use on a hundred hosts, because you will answer the prompt a hundred times.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play contains three command tasks. Running it with --check reports changed=0 and skipped=3. What has check mode established about those tasks?

  2. Q2. Which pre-flight option opens SSH connections to every targeted host?

  3. Q3. Which of these will NOT appear in --list-tasks output? Select all that apply.

  4. Q4. Resuming a failed play with --start-at-task is safe as long as you name a task that comes after the one that failed.

Passing score: 75%. Answers are checked in this browser.