Skip to main content
RunBook Academy

AnsibleXXV · Check Mode, Diff and Static ValidationCheck mode, diff and static validation

The sequence before a production run

Advanced⏱ ~24 minansible-playbookansible-lintansible-inventory

What you'll learn

  • Run the six rungs in order and state the stop condition for each
  • Name the defect class each rung catches and the class it is blind to
  • Assemble the evidence a change record should carry from a real run
  • Recognise when a rung has been satisfied without being read

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.

Each of the previous six lessons produced a control with a stated limit. This one puts them in order, because the ordering is where most of the value is: the rungs are arranged so that each one is cheaper than the next and rules out a class of mistake that would otherwise waste the next one’s time.

The ladder, with its blind spots

#RungCatchesBlind to
1--syntax-checkYAML errors, unresolvable module names, missing roles and static importsModule options, variables, template sources, dynamic includes
2ansible-lintDeprecated constructs, non-determinism, missing file modes, shell where a module existsAnything that depends on a value; anything computed at run time
3--list-hostsThe wrong target set, a pattern that matched more than intendedWhat would change; whether the hosts are reachable
4--check on one canaryConnectivity, authentication, escalation, guard evaluation, template resolution, a change countUnsupported modules, sequential dependencies, environmental preconditions, everything second-order
5--check --diff on one canaryThe exact content of predicted file changesWhether that content is correct or the service accepts it
6Real run under --limit to the canaryEverything the earlier rungs are blind to, on one hostBehaviour at scale, concurrency, load

The third column is the one to read. Every rung above is satisfiable without establishing safety, and the reason to write the blind spots down is that a ladder whose limits are undocumented gets treated as complete by whoever inherits it.

The sequence, as an actual procedure

Read-only / Saferungs 1 to 3 - free, connect to nothing
set -euo pipefail

PLAY=site.yml
INV=inventories/production/hosts
CANARY=web01.example.com

# 1. It compiles.
ansible-playbook -i "$INV" "$PLAY" --syntax-check

# 2. It is not obviously wrong. Fail on the target profile.
ansible-lint "$PLAY"

# 3. It targets who you think. READ THIS OUTPUT - do not just check the
#    exit code, which is 0 for any resolvable pattern including a wrong one.
ansible-playbook -i "$INV" "$PLAY" --list-hosts
ansible-playbook -i "$INV" "$PLAY" --list-tasks
Read-only / Saferungs 4 and 5 - one host, no changes
# 4 and 5 together. One host: diff output for a fleet is unreadable, and
#    the host whose diff differs from the rest is invisible in it.
ansible-playbook -i "$INV" "$PLAY" --limit "$CANARY" --check --diff
Service impact possiblerung 6 - the canary is a real change
ansible-playbook -i "$INV" "$PLAY" --limit "$CANARY"

# Then verify the outcome directly, on the host, not from Ansible output:
#   the service is running, the port answers, the log is clean, the
#   dependent system still works. Ansible reporting ok means the module
#   was satisfied, not that the service works.

The stop conditions

A rung is not “run it and look at the exit code”. Each has something to read and a condition that stops the sequence.

1. --syntax-check. Stop on any non-zero exit. Note that 120 means the inventory did not parse and 1 usually means a referenced file or role is missing — neither is a problem in the playbook body, and knowing that saves twenty minutes.

2. ansible-lint. Stop on a new finding in a file this change touches. Pre-existing findings elsewhere are debt, not a stop condition — that distinction is what makes the linter adoptable at all.

3. --list-hosts. Stop if the count differs from what you expected, in either direction. Fewer is as alarming as more: a pattern that matched twelve hosts when you expected fourteen means two are missing from the inventory, and the run will report complete success while leaving them behind.

4 and 5. --check --diff on the canary. Three separate reads:

  • The skipped count, against the modules you expect to be skipped. A surprise here means something is not being evaluated and you do not know what.
  • The diff hunks, line by line. This is a code review of the rendered output and it is the reason the rung exists.
  • The first task whose success depends on a previous task’s real effect. Everything after it is reported against a world that will not exist.

Stop if the diff contains a line you did not intend, or if the check run ended earlier than the play does.

6. The canary run. Stop unless the outcome verifies on the host. Not changed=3 in the recap: the service is running, the port answers, the dependent thing still works. This distinction — module satisfied versus service working — is the subject of the testing part, and it is the one that separates a canary from a slower deployment.

What the change record should carry

The output of the sequence is a paragraph, and it is a much better artefact than a screenshot of a green run:

Playbook:      site.yml @ 4f2a91c
Inventory:     inventories/production/hosts
Target:        webservers (14 hosts, confirmed with --list-hosts)
Static gates:  syntax-check pass; ansible-lint pass at profile 'safety'
Dry run:       --check --diff --limit web01.example.com
               18 of 22 tasks evaluated; 4 command tasks skipped
               (no check-mode support); 3 files predicted to change,
               diffs reviewed
Not validated: the 4 command tasks; anything after task 15, which
               depends on the package install
Canary:        web01.example.com, real run, service verified on host
Rollout:       --limit 'webservers:!web01.example.com', serial 4

Two lines in that are unusual and both are the point. “18 of 22 tasks evaluated” replaces “check mode passed” with something a reviewer can weigh. “Not validated” is a section most change records do not have, and it is the one that would have prevented the incidents in the previous lessons.

Knowledge check

Knowledge check · 4 questions

  1. Q1. --list-hosts reports 12 hosts and you expected 14. What is the correct response?

  2. Q2. Why does --limit to a single canary belong on the check rung rather than only on the real run? Select all that apply.

  3. Q3. A change record that states 18 of 22 tasks were evaluated and names the 4 that were not is more useful than one stating that check mode passed.

  4. Q4. A 30-minute change window cannot fit the full sequence plus the whole-fleet rollout. What should be cut?

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