AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting
What idempotent means, precisely
What you'll learn
- State the operational definition of idempotency used in this course
- Distinguish "did nothing" from "checked and found it already correct"
- Run the second-run test and read its result correctly
- Explain why a run that is merely harmless to repeat is not idempotent
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
“Idempotent” is used loosely to mean “safe to run twice”. That definition is too weak to be useful, because it is satisfied by a playbook that does nothing at all, and by one that does the same destructive thing twice with no cumulative effect.
This course uses a stronger definition, because the weaker one cannot support the things later parts need — drift detection, audit trails and CI gates all rest on the strong form.
The definition
A playbook is idempotent when both of these hold:
- It converges. Running it against a host in any starting state brings that host to the declared state, or fails loudly trying.
- It reports zero changes on a converged host. A subsequent run,
with the same inputs, against a host nothing else has touched,
reports
changed=0.
The second clause is the one people drop, and it is the one that does the work. Convergence alone gives you a playbook you can rerun. Convergence plus accurate reporting gives you a playbook whose output is evidence.
“Did nothing” and “checked and found it correct” are not the same
Both produce ok in the recap. They are completely different facts
about the estate.
- Did nothing. The task was skipped, or the module was not invoked, or a condition was false. Nothing was inspected. Nothing is known.
- Checked and found it already correct. The module connected, read the current state, compared it with the declaration, and found no difference. That is a positive assertion that the host matches the declaration.
The second is a proof. It is the single most valuable thing a configuration management run produces, and the reason a no-op run over the fleet is worth scheduling.
The recap does not distinguish them, which is why skipped deserves the
attention it gets in the recap lesson. A task skipped by a false when:
contributes nothing to your knowledge of the host; a task that reported
ok after a real comparison contributes a verified fact.
The second-run test
The test is exactly what it sounds like. Run the play; run it again; read the second recap.
ansible-playbook -i inventory.ini site.yml --limit staging
ansible-playbook -i inventory.ini site.yml --limit stagingThe pass condition is changed=0 on every host in the second recap. Any
other result is one of exactly three things, and telling them apart is
the skill:
| Second run shows | Cause | Fix |
|---|---|---|
changed on a command/shell task | The module cannot detect its own effect | Add creates, or accurate changed_when — lesson 4 |
changed on a declarative module | The declaration and the module’s normalisation disagree — a file mode written as 644 rather than '0644', a template with a timestamp in it, a package pinned to latest | Fix the declaration |
changed on a task you did not expect | Something else on the host is fighting you — another CM tool, a cron job, a vendor agent | An estate problem, not a playbook problem |
The third row is the interesting one, and it is the reason the test is worth running even on a playbook you trust.
Demonstrating the shape
Here is the two-sided behaviour in a single measured run. The same
command, fenced by creates against a path that does not exist and
against one that does:
$ ansible-playbook -i inv2.ini creates.ymlTASK [One-shot step, marker absent] ********************************************
changed: [localhost]
TASK [One-shot step, marker present] *******************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Two identical commands. One reported changed, one reported ok, and
the only difference between them was whether the state they establish
was already present. That is convergence, compressed into one run: the
first task is what a first run looks like, the second is what a second
run looks like.
The verbose result of the second task shows the module’s reasoning:
$ ansible-playbook -i inv2.ini creates.yml -vok: [localhost] => {"changed": false, "cmd": ["/bin/echo", "would-have-run"], "msg": "Did not run command since '/etc/hostname' exists", "rc": 0, "stdout": "skipped, since /etc/hostname exists"}"changed": false with a stated reason. That is a module doing the
read-compare-act cycle on behalf of a command that cannot do it itself.
What idempotency is not
It is not “the second run does no harm”. A play that reinstalls a
package from scratch every time does no harm and is not idempotent. It
reports changed forever, restarts services that did not need
restarting, and consumes a maintenance window’s worth of time on an
estate that was already correct.
It is not “the tasks are all modules”. Modules are usually
idempotent, but ansible.builtin.file with state: touch is documented
to give “an existing file or directory … updated file access and
modification times”. It changes the host on every run, so it reports
changed on every run — a module, and not idempotent in this sense.
It is not a property you can assert. It is a property you measure, by running the play twice.
Knowledge check
Knowledge check · 4 questions
Q1. Which of these plays is idempotent by the definition used in this course?
Q2. A task reports ok on the second run. What has it established about the host?
Q3. A second run reports changed on one task. Which are plausible causes? Select all that apply.
Q4. A task using ansible.builtin.file with state: touch reports changed on every run, so being a module rather than a shell task does not by itself make a task idempotent.
Passing score: 75%. Answers are checked in this browser.