Server

Ansible Idempotency and Check Mode

By Prabath Thalangama· October 24, 2026· 3 min read
#ansible#automation#idempotency

Introduction

Idempotency means running the playbook again produces no changes because the system already matches the desired state. It's what makes Ansible safe to run on a schedule and easy to reason about. Most modules are idempotent by default; the gaps are command, shell, and sloppy changed_when.

How Ansible decides "changed"

Each task returns one of ok (already correct), changed (it modified something), failed, or skipped. A well-written module checks current state first and only acts on a delta:

- name: Ensure line in sysctl        # idempotent — checks the file
  ansible.posix.sysctl:
    name: net.ipv4.ip_forward
    value: "1"
    state: present
    reload: true

Run twice: first changed, second ok.

The command/shell problem

- name: Extract release            # runs EVERY time, always "changed"
  ansible.builtin.command: tar xzf /tmp/app.tgz -C /opt/app

command has no idea whether the work is needed. Fix it:

- name: Extract release
  ansible.builtin.command: tar xzf /tmp/app.tgz -C /opt/app
  args:
    creates: /opt/app/VERSION        # skip if this path already exists

- name: Remove lockfile
  ansible.builtin.command: /opt/app/bin/unlock
  args:
    removes: /opt/app/.lock          # skip if the path is absent

For commands with no natural sentinel file, control the status explicitly:

- name: Check migration status
  ansible.builtin.command: /opt/app/bin/migrate --status
  register: migrate_status
  changed_when: false                 # a read — never a change

- name: Run migrations
  ansible.builtin.command: /opt/app/bin/migrate
  when: "'pending' in migrate_status.stdout"
  changed_when: true
  failed_when: migrate_status.rc not in [0, 2]

Rules of thumb:

  • A task that only reads → changed_when: false.
  • A task guarded by when: on a prior check → set changed_when to reflect what it did.
  • Prefer a real module (ansible.builtin.unarchive, git, copy) over command — they're idempotent for free.

Check mode (--check)

--check runs the playbook without making changes, reporting what would change. --diff shows the content diff for file/template tasks.

ansible-playbook site.yml --check --diff

Caveats:

  • Modules that don't support check mode are skipped in --check (you see skipping), so downstream tasks that depend on their result may behave oddly.
  • command/shell are skipped in check mode unless you set check_mode: false (force them to run — only safe for read-only commands).
  • A task can be forced to always run even in check mode, or never:
- name: Gather current version (safe to always run)
  ansible.builtin.command: /opt/app/bin/version
  register: appver
  check_mode: false
  changed_when: false

Verification and troubleshooting

# The real test: run twice, second run must be all ok/skipped
ansible-playbook site.yml
ansible-playbook site.yml | grep -E 'changed=|failed='   # changed=0 on the second run

ansible-lint          # flags command-instead-of-module, missing changed_when
  • Second run still shows changed — a command/shell without creates:/changed_when:, a lineinfile regex that doesn't match its own output, a template with a timestamp/random value in it, or file with mode specified in a form that never quite matches (0644 vs 644 — quote it).
  • --check reports no changes but a real run changes things — a task skipped in check mode (unsupported module) hid a downstream change.
  • --check fails where a real run succeeds — a later task depends on a file an earlier skipped task would have created. Add check_mode: false to the safe prerequisite, or guard the dependent task.
  • changed_when: false hid a real failure — that only affects the changed/ok status, not failed. If the command's non-zero exit should fail the play, don't also set failed_when: false.
  • Handlers fire on every run — a task notifying them is falsely reporting changed (see above). Fix the task's idempotency and the handler noise goes away.
PrabathStuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.