How do changed_when, creates/removes, check mode, and diff mode keep Ansible playbooks idempotent?
Quick Answer
These tools work together to make playbooks safe to run repeatedly. changed_when overrides default change detection, creates/removes skip tasks when files already exist, check mode simulates runs without touching anything, and diff mode shows exactly what would change.
Detailed Answer
Think of a thermostat. Setting it to 72 degrees is idempotent -- no matter how many times you press the button, the system targets 72 and only acts if the current temperature is different. An Ansible playbook should work the same way: running it ten times should produce the same result as running it once, with no unnecessary changes on repeat runs. The tools Ansible gives you for this are like the thermostat's sensor that checks the room temperature before turning on the heater.
Most built-in Ansible modules are naturally idempotent. The copy module checks file checksums before copying, the user module checks user attributes before making changes, and the yum module checks installed package versions before installing. But the command and shell modules always report 'changed' because Ansible has no way to know whether a shell command actually modified anything. This is where changed_when, creates, and removes come in. changed_when takes a Jinja2 expression that decides whether the task really changed something, based on command output. creates tells Ansible to skip the task if a certain file already exists. removes tells Ansible to skip the task if a certain file is missing.
Under the hood, Ansible evaluates idempotency at the task level. Each module receives the current state of the target, compares it to the desired state, and reports ok (no change needed), changed (something was modified), or failed. Check mode (--check) tells modules to do the comparison but skip the actual change, reporting what would happen. Diff mode (--diff) goes further by showing the actual differences, like a unified diff for file content. Together, check mode and diff mode let you preview a playbook run against production without making any changes -- which is essential for change management workflows that need pre-approval.
At production scale, idempotency is not just nice to have -- it is a reliability requirement. Configuration management runs should be scheduled (through AWX or cron) to continuously enforce the desired state. If a playbook is not idempotent, every scheduled run creates false 'changed' notifications. This leads to alert fatigue, triggers unnecessary handler restarts, and makes it impossible to tell real drift from playbook noise. Teams should track changed task counts across runs -- a truly idempotent playbook running against an already-configured system should report zero changes.
The sneaky gotcha is that some modules are only idempotent under specific conditions. The lineinfile module with regexp matching can be idempotent, but if the regex matches multiple lines or the line content includes values that change between runs (like timestamps), it breaks idempotency. The template module is idempotent for file content but always reports changed if file permissions differ from expected -- even when the content is identical. Architects should run playbooks twice in CI and add assertion tasks that verify the second run reports zero changes, catching idempotency problems before they reach production.