Skip to main content
RunBook Academy

AnsibleXII · Idempotency and Change ReportingIdempotency and change reporting

changed=0 as the cheapest signal an estate produces

Intermediate⏱ ~16 minansible-playbook

What you'll learn

  • Explain why an accurate changed=0 is evidence rather than an absence of output
  • Design a scheduled no-op run that acts as a drift detector
  • Implement the second-run test as a CI gate
  • Recognise how an inaccurate changed raises the noise floor of the whole estate

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.

Here is the payoff for the previous six lessons.

When every task in a play reports change accurately, running that play against a converged fleet produces a recap of changed=0 on every host. And that line, which looks like the absence of information, is the strongest routine statement anyone in your organisation can make about the estate:

Every resource this play declares was checked on every one of these hosts, and every one of them matched.

Nothing else you run produces that. A monitoring check tests a handful of symptoms. A compliance scan tests a policy. This tests the whole declaration, on every host, and it costs one scheduled job.

Two lines, one of which is evidence

Read-only / Safethe shape of a converged fleet
$ ansible-playbook -i inventory.ini site.yml
PLAY RECAP *********************************************************************
web01.example.com          : ok=42   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web02.example.com          : ok=42   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web03.example.com          : ok=42   changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Illustrative output

ok=42 changed=0 per host: forty-two declarations verified, none needed correcting.

Now the same recap from a play with three command tasks nobody has fenced:

Read-only / Safethe same fleet, with noise
$ ansible-playbook -i inventory.ini site.yml
PLAY RECAP *********************************************************************
web01.example.com          : ok=42   changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web02.example.com          : ok=42   changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
web03.example.com          : ok=42   changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Illustrative output

The second fleet may be in exactly the same state as the first. The difference is that nobody can tell — and, crucially, nobody can tell on the night when the number is 4.

The noise floor

That is the mechanism by which inaccurate reporting destroys the signal, and it is worth stating plainly because it is gradual.

A play with a permanent changed=3 has a noise floor of three. Any genuine drift has to push the number above three to be noticed, and somebody has to be looking closely enough to notice that 4 is not 3. Nobody is. Within a few weeks the number is background, and within a few months the recap is not read at all.

The rot is cumulative across the estate. Ten plays each with a small permanent changed produce a dashboard where every run is amber, and an amber dashboard is an ignored dashboard. The individual defects were each defensible; the aggregate is an estate with no drift detection.

The scheduled no-op run

The design is deliberately boring:

  • Run the full playbook on a schedule against the whole fleet.
  • Expect changed=0, failed=0, unreachable=0 everywhere.
  • Alert on anything else.

Two decisions to make, and both are real trade-offs.

Enforce, or only report? A scheduled run that applies changes corrects drift automatically, which is what configuration management is for — and it also means the fleet changes at 02:00 without anyone deciding to change it. A scheduled --check run reports drift without correcting it, at the cost of the check-mode gaps covered in lesson 4: tasks without check-mode support are skipped rather than compared.

The honest position is that these are different tools. Report-only is where most estates should start, because the first few weeks of drift reports are usually surprising. The drift part covers moving to enforcement.

What to alert on. changed on a scheduled run is either drift or a reporting bug, and both need a human. Alerting on “any host with changed > 0” is the right first rule, and it is only tolerable because the reporting is accurate. That is the dependency: the alert is only worth having if the previous six lessons were applied.

The second-run test as a CI gate

The same property, asserted mechanically before the code merges. Run the play twice against a disposable host and fail the build if the second run reports any change:

Configuration changeidempotency gate for CI
set -euo pipefail

ansible-playbook -i inventory.test.ini site.yml

ansible-playbook -i inventory.test.ini site.yml | tee second-run.log

if grep -qE 'changed=[1-9]' second-run.log; then
echo "FAIL: second run reported changes; the play is not idempotent"
exit 1
fi

echo "OK: second run reported no changes"

Three things about that script are deliberate.

set -euo pipefail means the first run failing fails the job, rather than the script continuing to a second run that will fail confusingly.

The grep is against changed=[1-9], which matches changed=1 through changed=9 and also the first digit of larger numbers — it does not match changed=0. Matching the recap text is crude, and the robust version parses the JSON output of a callback plugin instead; the testing part covers that. The crude version is still worth having on day one.

The test runs against a disposable host. A second-run test that converges production twice is not a test, it is two deployments.

What changed=0 still does not prove

Being precise about the limits keeps the signal honest.

  • It does not prove the declaration is correct. A play that declares the wrong TLS configuration will converge every host onto it and report changed=0 forever. Idempotency is agreement between the host and the declaration; it says nothing about whether the declaration is right.
  • It does not cover what the play does not declare. Attributes nobody mentioned are not compared. ok=42 is forty-two declarations, not the whole host.
  • It does not cover hosts absent from the recap. A host excluded by --limit, or dropped by a pattern typo, contributes no lines and no alarm.
  • It does not survive an inaccurate changed_when: false. A task that writes and reports nothing contributes a false ok, which is worse than contributing noise.

Counting recap lines against the expected host count, and reviewing what the play actually declares, are the two habits that keep those limits from becoming blind spots.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A scheduled run over a converged fleet reports ok=42 changed=0 on every host. What has been established?

  2. Q2. A play permanently reports changed=3 because of three unfenced command tasks. What is the cost?

  3. Q3. Which are genuine limits of a changed=0 result? Select all that apply.

  4. Q4. A scheduled --check run is a weaker drift signal than a scheduled real run, because modules without check-mode support are skipped rather than compared.

Passing score: 75%. Answers are checked in this browser.