Skip to main content
RunBook Academy

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

How check mode works

Intermediate⏱ ~22 minansible-playbook

What you'll learn

  • Predict whether a task simulates from the CLI flag and the check_mode keyword together
  • Use check_mode true to force simulation and check_mode false to force a real run
  • Explain why ansible_check_mode can be False in a task that is simulating
  • State what a --check run does on the managed node despite changing nothing

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.

Whether a given task simulates or executes is decided by two independent controls, and a third thing that only looks like it is one of them.

ControlScopeEffect
--check / -CThe whole invocationEvery task simulates, unless a keyword says otherwise
check_mode: true / falsePlay, block or taskOverrides the flag, in either direction, for what it covers
ansible_check_modeRead-only variableReports whether --check was passed — not whether this task is simulating

The next lesson argues about what a clean --check run does and does not prove. This one is about getting the mechanics exactly right, because half of the misplaced confidence in check mode comes from a play whose author believed a task was simulating when it was not, or the reverse.

The four combinations

- name: check_mode combinations
  hosts: webservers
  tasks:
    - name: An ordinary task
      ansible.builtin.command: /usr/local/bin/probe

    - name: A read-only probe that must run during a check
      ansible.builtin.command: /usr/local/bin/app --version
      register: app_version
      changed_when: false
      check_mode: false

    - name: A task that never runs for real
      ansible.builtin.command: /usr/local/bin/destroy-everything
      check_mode: true
TaskWithout --checkWith --check
OrdinaryExecutesSimulates
check_mode: falseExecutesExecutes
check_mode: trueSimulatesSimulates

check_mode: false is the one to be careful with — it is a task that runs for real during a dry run. check_mode: true is the safer of the two, and both directions have legitimate uses.

check_mode: false — the read-only probe

The problem it solves appears the moment a play branches on something it measured. Two identical probes, one with the keyword and one without, during a --check run:

Read-only / Safeone probe runs, the other is skipped
$ ansible-playbook -i localhost, probe.yml --check
TASK [A read-only probe that must run even during a dry run] *******************
ok: [localhost]

TASK [The same probe without check_mode false] *********************************
skipping: [localhost]

TASK [What the two probes returned] ********************************************
ok: [localhost] => {
  "msg": [
      "with check_mode false: 4.2.1",
      "without it: "
  ]
}

The keyword makes the probe run, and the registered value is real:

- name: Read the currently installed version
  ansible.builtin.command: /usr/local/bin/app --version
  register: app_version
  changed_when: false
  check_mode: false

changed_when: false belongs there too. The task genuinely changes nothing, so it should not report changed in a real run either — and the two keywords together are the signature of a correctly written probe.

check_mode: true — forcing simulation

The reverse keyword pins a task to simulation whether or not --check was passed. Its honest use is narrow: a task nobody should run yet, kept in the play so it is reviewed and versioned, or the destructive half of a play during a rehearsal.

Configuration changea real run in which one task still simulates
$ ansible-playbook -i inv.ini deploy.yml
TASK [A task that never runs for real, even without --check] *******************
skipping: [localhost]

TASK [What the forced-simulation task returned] ********************************
ok: [localhost] => {
  "msg": {
      "changed": false,
      "cmd": ["/bin/echo", "would-have-run"],
      "failed": false,
      "msg": "Command would have run if not in check mode",
      "rc": 0,
      "skipped": true,
      "stdout": ""
  }
}

It applies at block and play level too, which is how you rehearse the dangerous half of a play while the preparation runs for real:

- name: Prepare for the migration
  hosts: dbservers
  tasks:
    - name: Take a backup            # runs for real
      ansible.builtin.command: /usr/local/bin/backup.sh
      args:
        creates: '/backups/{{ inventory_hostname }}-{{ run_id }}.dump'

    - name: The migration itself     # simulated until we mean it
      block:
        - name: Apply the schema change
          community.postgresql.postgresql_query:
            db: app
            query: "{{ lookup('file', 'migration.sql') }}"
      check_mode: true

Where ansible_check_mode disagrees with reality

ansible_check_mode is a magic variable, and the documented pattern is to guard on it:

- name: Send the deployment notification
  ansible.builtin.uri:
    url: 'https://chat.example.com/api/notify'
    method: POST
    body_format: json
    body:
      text: 'Deployment complete on {{ inventory_hostname }}'
  when: not ansible_check_mode

That is correct and useful — a dry run should not page the team. What it is not is a reliable statement about whether the surrounding task is simulating.

Read-only / Safea play forced into check mode by keyword
$ ansible-playbook -i localhost, forced.yml
TASK [A command that will never run] *******************************************
skipping: [localhost]

TASK [Report] ******************************************************************
ok: [localhost] => {
  "msg": "skipped=True check_mode=False"
}

The task simulated. ansible_check_mode is False.

And the mirror image, with the flag on the command line and the keyword turning one task off:

Read-only / Safea task running for real during a check run
$ ansible-playbook -i localhost, probe.yml --check
TASK [A task forced to run for real during a check run] ************************
ok: [localhost] => {
  "msg": "inside check_mode false, ansible_check_mode is True"
}

Stated plainly: ansible_check_mode reports the command-line flag, not the effective mode of the task reading it. Use it for “was this invocation a dry run” — which is what a notification guard wants. Do not use it to reason about whether a particular task executed.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A play carries check_mode: true and is run without --check. Inside it, a task reads ansible_check_mode. What does it get?

  2. Q2. A command probe is skipped during a --check run because it has no check-mode support. Which of these are true of the registered variable? Select all that apply.

  3. Q3. A --check run connects to every targeted host and executes module code there, with become if the task has it.

  4. Q4. Which task is an appropriate place for check_mode: false?

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