AnsibleXXV · Check Mode, Diff and Static ValidationCheck mode, diff and static validation
What a clean --check does not prove
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
This lesson exists because of a sentence that gets said in change reviews, in this form or a close variant:
We ran it with
--checkand 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
$ 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=0failed=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.
$ 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=0A 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:
$ ansible-playbook -i inv.ini render.ymlTASK [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=0copy 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
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?
Q2. Which of these does a clean --check run genuinely establish? Select all that apply.
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.
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.