Skip to main content
RunBook Academy

AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting

What idempotent means, precisely

Intermediate⏱ ~14 minansible-playbook

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

Not yet marked complete on this device.

“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:

  1. It converges. Running it against a host in any starting state brings that host to the declared state, or fails loudly trying.
  2. 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.

Configuration changethe second-run test
ansible-playbook -i inventory.ini site.yml --limit staging
ansible-playbook -i inventory.ini site.yml --limit staging

The 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 showsCauseFix
changed on a command/shell taskThe module cannot detect its own effectAdd creates, or accurate changed_when — lesson 4
changed on a declarative moduleThe 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 latestFix the declaration
changed on a task you did not expectSomething else on the host is fighting you — another CM tool, a cron job, a vendor agentAn 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:

Read-only / Safethe two sides of convergence, in one play
$ ansible-playbook -i inv2.ini creates.yml
TASK [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=0

Two 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:

Read-only / Safewhy the second task did not run
$ ansible-playbook -i inv2.ini creates.yml -v
ok: [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

  1. Q1. Which of these plays is idempotent by the definition used in this course?

  2. Q2. A task reports ok on the second run. What has it established about the host?

  3. Q3. A second run reports changed on one task. Which are plausible causes? Select all that apply.

  4. 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.