Chapter 3: Modules & Ad-hoc Commands
Topic 18

Check Mode and Diff Mode

WorkflowSafety

ansible-playbook --check runs a play without changing anything — every module reports what it would do against the live machine, and --diff shows the exact before/after of any file it would touch. This is the closest thing Ansible has to a plan.

Since there is no state file to read, the dry run works by having each module inspect the real node and predict its change, so --check --diff is your pre-flight against the actual server, not against a recorded snapshot. It is the no-state-file equivalent of the planning step a provisioning tool runs before it applies.

--check as a Dry Run

Under --check, every module runs in predict-only mode: it reports changed or ok as if it had acted, but writes nothing. Running the Larkspur web play under --check tells you whether a real run would restart gunicorn or rewrite larkspur.conf before you let it touch anything.

The prediction comes from the same machinery the last topics described. Each module reads the live host and compares to the declared state — exactly what it does on a real run — and stops short of writing. The changed/ok it reports is the honest one, just without the side effect.

--diff for File Changes

Paired with --check, --diff prints a unified diff of what template, copy, or lineinfile would change in app.env or the nginx config. You read the literal lines that would change, which is the review a config push deserves.

A bare changed count tells you that a file would change; the diff tells you what. Those are different facts, and the second one is what actually lets you approve the push — you are signing off on specific lines, not on a number.

Two flags that together stand in for a plan
--check
Dry run — every module predicts changed or ok against the live host but writes nothing.
--diff
Prints the exact before/after lines a file would change, so you approve specific content, not a count.
Together
A state-free pre-flight against the real server — Ansible's stand-in for a provisioning tool's plan.

The Dry Run That Substitutes for plan

A provisioning tool reads its state file to compute a plan; Ansible has no state, so --check computes "what would change" by having each module query the live host directly. The prediction is against reality on the spot, which is why it can catch a hand-edited drift a stored plan would miss.

This is the spine of the course showing up as a feature. No state file means the dry run cannot be stale relative to the world, because it never consulted a recorded snapshot — it asked the machine. The trade-off, covered below, is that it predicts one task at a time.

Modules That Do Not Support Check Mode

command and shell cannot predict their effect — Ansible has no idea what an arbitrary command would do without running it — so under --check they skip by default and report nothing useful. A play built on raw commands therefore has a weak dry run.

This is one more reason to prefer real modules, beyond the idempotency argument from two topics back. A real module implements check mode honestly and shows you its prediction; a raw command leaves a blank where the most important file change might be.

The Limits of the Prediction

--check is per-task against current state, so a task whose outcome depends on an earlier task's change can mispredict — the earlier change did not actually happen in check mode, so the later task sees the unchanged host. A task that edits a file created by a previous task may report differently in check than in a real run.

The dry run is a strong sanity pass, not a guarantee that a real run is identical. Treat a clean --check as "no obvious surprises in single tasks," not as "this run is proven safe end to end," and keep that limit in mind for plays with inter-task dependencies.

Where It Fits

--check --diff is the gate before a production push: run it on the canary host, read the diff, confirm only the intended files and services change, then run for real. It is the habit that turns "I think this play is safe" into "I read exactly what it does."

Make it a reflex for any config push. The web play that rewrites app.env and might restart gunicorn is exactly the kind of change you want to see in diff form on one host before it reaches the fleet, and the cost of running it first is a few seconds.

Common Mistakes
  • Treating --check as a guarantee the real run is identical, then being surprised when a task that depended on an earlier change — skipped in check — behaves differently in the actual run.
  • Pushing a template change to app.env on the web tier without --check --diff first, then discovering only after the fact that it rewrote more lines than intended and triggered an unwanted gunicorn restart.
  • Building a play on command/shell and expecting a meaningful dry run, when those modules skip under --check and tell you nothing about what they would do.
  • Running --check against all to "see what would happen" and assuming a clean report means safe, ignoring that check mode cannot model the inter-task dependencies the real run will hit.
  • Forgetting --diff and reading only the changed count, so you know that a file would change but not what in it — the diff is the part that actually lets you approve the push.
Best Practices
  • Run ansible-playbook --check --diff on the canary host before any production push, and read the diff to confirm only the intended files and services change.
  • Prefer real modules over command/shell partly so your dry run has teeth — real modules implement check mode and report honestly, raw commands do not.
  • Treat --check as the no-state-file equivalent of a provisioning plan: a live prediction against the actual server, strong for single tasks, weaker across task dependencies.
  • Read --diff output on the literal config lines — larkspur.conf, app.env — rather than trusting a bare changed count, since the lines are what you are actually approving.
  • Keep the limit in mind for plays with inter-task dependencies, and do not read a clean check as proof the full run is identical.
Comparable tools Terraform plan the direct provisioning-side analog, with the state-file difference Puppet --noop the same dry-run idea for an agent tool Shell script no dry run at all, the gap real modules close

Knowledge Check

What do --check and --diff each show, and why do they pair?

  • --check reports what a module would change, writing nothing; --diff prints before/after, so you see whether and what changes
  • --check just validates the YAML syntax of the entire whole play and --diff then compares two separate playbook files
  • --check runs the whole play through twice over and --diff then reports the difference it observed between the two separate runs
  • --diff applies the change to the live target host and --check then rolls it back fully automatically should it ever fail

Why does Ansible's --check substitute for a state-file plan despite having no state?

  • Each module queries the live host to predict its change, so the prediction is against reality, not a recorded snapshot
  • Ansible quietly keeps a hidden per-host state file just for the check mode run and then discards it again right afterward
  • Check mode reads back the very last play recap on disk to infer what each task happened to change previously
  • It compares the current playbook against an earlier cached copy stored away on the control node itself

What is the failure mode when one task depends on another in check mode?

  • The earlier change is not applied in check, so the dependent task sees an unchanged host and can mispredict its result
  • Check mode just flatly refuses outright to run any play at all that happens to have inter-task dependencies built right into it
  • The dependent task is just silently skipped right over and then never reported anywhere at all in the run's output
  • Ansible quietly applies the earlier change for real, purely to keep the later task's prediction accurate

Why do command and shell weaken a dry run?

  • They cannot predict their effect without running, so they skip under --check and report nothing useful about what they'd do
  • They run for real even under a --check dry run, which makes that supposedly safe dry run genuinely dangerous
  • They produce a diff so enormous and so noisy that it ends up completely hiding the real changes you care about
  • They forcibly drag the whole rest of the running play right back out of check mode the very moment that they actually execute

You got correct