How does Ansible idempotency work, and why does it matter?
Quick Answer
Idempotency means running the same playbook multiple times gives the same result with no unwanted side effects. Each module checks the current state before acting and only makes changes when reality differs from what you asked for.
Detailed Answer
Think of idempotency like a thermostat. You set the desired temperature to 72 degrees. If the room is already at 72, the thermostat does nothing. If it is at 68, it turns on the heater. If you press the 72-degree button ten more times, nothing extra happens because the desired state is already reached. Ansible modules work the same way -- they check the current state, compare it to what you want, and only make changes when there is a gap.
Idempotency is a concept borrowed from math: an operation is idempotent if doing it multiple times has the same effect as doing it once. In Ansible, this idea is built into every well-written module. When you write a task like 'ensure nginx is installed,' the apt module first checks whether nginx is already there. If it is, the task reports 'ok' (no change). If it is not, the module installs it and reports 'changed.' This check-first-then-act pattern is the foundation of Ansible's approach -- you describe the end state you want, not the steps to get there.
Under the hood, each module has its own logic for checking state. The file module looks at permissions, ownership, and content hashes before touching anything. The template module generates output from a Jinja2 template and compares its checksum against the existing file on the remote host. The user module queries the system's user database before creating or modifying accounts. The service module checks whether a service is running before starting or stopping it. This per-module checking is what lets Ansible produce accurate change reports -- the 'changed' count in the play recap tells you exactly how many tasks actually modified the system.
In production, idempotency is critical for several reasons. First, it makes re-runs safe -- if a playbook fails halfway through, you can re-run it from the start without worrying about duplicate package installs, duplicate config entries, or services getting restarted twice. Second, it supports drift detection -- by running playbooks in check mode (--check), you can see what would change without actually changing it, effectively auditing your infrastructure for unexpected differences. Third, it fits naturally into CI/CD pipelines where playbooks might be triggered multiple times by retries or overlapping deployments.
The biggest pitfall is the shell and command modules -- they are not idempotent by default because Ansible cannot know what a random shell command does. Running 'echo data >> /etc/config' via the shell module will add duplicate lines on every run. To fix this, use purpose-built modules (lineinfile, template, copy) instead of shell commands whenever possible. When shell is unavoidable, use the creates or removes parameter to add a condition, or use changed_when to control when Ansible marks the task as changed. Another subtle trap is using 'latest' as the package state -- while technically idempotent, it can cause surprise upgrades in production if upstream repositories are updated between runs.