Ansible Idempotency and Check Mode
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 → setchanged_whento reflect what it did. - Prefer a real module (
ansible.builtin.unarchive,git,copy) overcommand— 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 seeskipping), so downstream tasks that depend on their result may behave oddly. command/shellare skipped in check mode unless you setcheck_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— acommand/shellwithoutcreates:/changed_when:, alineinfileregex that doesn't match its own output, a template with a timestamp/random value in it, orfilewithmodespecified in a form that never quite matches (0644vs644— quote it). --checkreports no changes but a real run changes things — a task skipped in check mode (unsupported module) hid a downstream change.--checkfails where a real run succeeds — a later task depends on a file an earlier skipped task would have created. Addcheck_mode: falseto the safe prerequisite, or guard the dependent task.changed_when: falsehid 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 setfailed_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.
Related tools and reading¶
- On-site: YAML validator.
- Related posts: Structuring an Ansible playbook you can maintain, Terraform state explained.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in DevOps & CI/CD
OpenTelemetry for Distributed Tracing
A trace follows one request across services. OpenTelemetry is the vendor-neutral way to produce them — here's the model, the Collector, and sampling that won't bankrupt you.
October 24, 2026pre-commit Hooks That Actually Help
pre-commit runs checks before a commit lands, catching formatting, secrets, and syntax errors locally. Here's a config that helps without being annoying.
October 24, 2026Chaos Engineering Basics
Chaos engineering is controlled experiments that inject failure to verify your system tolerates it. Here's the method, safe blast radius, starter experiments, and game days.
October 24, 2026