Skip to main content
RunBook Academy

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

What a clean --check does not prove

Advanced⏱ ~24 minansible-playbook

What you'll learn

  • Name the four classes of defect a clean check run does not detect
  • Read a green check run and identify what it left unexamined
  • Explain why a check run of a sequential play reports on a world that will not exist
  • State accurately what a clean check run does establish

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.

This lesson exists because of a sentence that gets said in change reviews, in this form or a close variant:

We ran it with --check and it came back clean.

It is offered as evidence that the change is safe, it is accepted as evidence that the change is safe, and it is not evidence that the change is safe. The documentation is unusually direct about why:

Check mode is just a simulation. It will not generate output for tasks that use conditionals based on registered variables (results of prior tasks).

Four classes of defect pass a clean check run. Each one is demonstrated below on ansible-core 2.21.3.

1. Tasks that were never evaluated

Modules without check-mode support are, in the documentation’s words, “modules that do not support check mode report nothing and do nothing”. They come back as skipping, which is the same word a false when: produces.

The consequence for reading a recap: skipped is a count of two completely different things, and the modules most likely to be in it — command, shell, script, raw — are the ones doing the work you most wanted previewed. The module-model part covers the support levels and how to look them up before a change window; the point here is narrower and worse.

2. A play whose later tasks depend on its earlier effects

This is the class the documentation sentence is about, and it is the one that produces confidently wrong output rather than missing output.

- name: What a dry run does to a play that depends on its own effects
  hosts: appservers
  tasks:
    - name: Create the marker the rest of the play depends on
      ansible.builtin.command: "touch {{ marker }}"

    - name: Look for the marker
      ansible.builtin.stat:
        path: "{{ marker }}"
      register: marker_stat

    - name: Decide what to do next based on the marker
      ansible.builtin.debug:
        msg: "marker present: {{ marker_stat.stat.exists }}"

    - name: Only run when the marker is present
      ansible.builtin.debug:
        msg: "This is the task that does the real work."
      when: marker_stat.stat.exists
Read-only / Safethe dry run that lied
$ ansible-playbook -i inv.ini marker.yml --check; echo "exit=$?"
TASK [Create the marker the rest of the play depends on] ***********************
skipping: [localhost]

TASK [Look for the marker] *****************************************************
ok: [localhost]

TASK [Decide what to do next based on the marker] ******************************
ok: [localhost] => {
  "msg": "marker present: False"
}

TASK [Only run when the marker is present] *************************************
skipping: [localhost]

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

exit=0

failed=0, changed=0, exit 0. On a dashboard that is a clean run.

What actually happened: task one was skipped because command cannot predict an arbitrary program. Task two then measured a world in which task one had not run, and correctly reported that the marker is absent. Task three printed False. Task four — the task that does the real work — was skipped on the strength of it.

The check run reported on a hypothetical world: the one where the first task does not happen. That world will never exist. In the real run task one creates the marker, marker_stat.stat.exists is true, and the task that check mode never evaluated is the one that runs.

3. Preconditions the module does not evaluate in check mode

The previous class at least leaves a visible trail of skips. This one produces a check run that is not merely uninformative but actively encouraging.

Read-only / Safethree changes predicted, with a diff
$ ansible-playbook -i inv.ini render.yml --check --diff; echo "exit=$?"
TASK [Render the application config] *******************************************
--- before
+++ after: /etc/app/app.conf
@@ -0,0 +1,4 @@
+[database]
+host = db01.example.com
+user = appuser
+password = REDACTED

changed: [localhost]

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

exit=0

A reviewer would approve that. Three predicted changes, a readable diff of exactly what will be written, nothing skipped, exit 0.

Here is the same play, run for real, with nothing else changed:

Configuration changethe real run, immediately
$ ansible-playbook -i inv.ini render.yml
TASK [Render the application config] *******************************************
fatal: [localhost]: FAILED! => {"changed": false, "checksum": "12abdd31cf62e1aa0544a8ee68e9bddeef51ff5f", "msg": "Destination directory /etc/app does not exist"}

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

copy in check mode compares the content it would write against the content that is there, decides they differ, and reports changed. It does not verify that the destination directory exists, because verifying the environment is not what check mode is for — check mode answers would this produce a change, not would this succeed.

Those are different questions and the output does not distinguish them. A predicted change is a statement about desired state, not a prediction of success.

4. Everything second-order

The class that no amount of module support could ever address.

Check mode tells you a config file would change. It cannot tell you the service will start with the new content. It cannot tell you the new value is correct, that the port is free, that the certificate matches the hostname, that the downstream service can parse what you are about to give it, or that four hundred hosts restarting within the same minute will not overwhelm the thing behind them.

--diff narrows this usefully — it shows the content rather than the fact of a change — but “these three lines will change” is a statement about the file, and every consequence of the change lives outside the file.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A --check --diff run reports changed=3, prints a readable diff of the file to be written, and exits 0. The real run fails on the first task. What is the most likely explanation?

  2. Q2. Which of these does a clean --check run genuinely establish? Select all that apply.

  3. Q3. A command task skipped during a check run leaves its registered variable undefined, so downstream conditions fail loudly rather than silently taking the wrong branch.

  4. Q4. A check run of a deployment play fails at task nine because task three could not run in check mode. What is the best response?

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