Idempotency Is an Availability Property

Scenario: A scheduled convergence run bounces a payment service every night at 02:00. Nothing has changed on any of those nights. Three months of nightly ten-second outages went unexplained.

New words, in plain English

By Phase 4 idempotency should feel like hygiene. It is more than that: once handlers, scheduled runs and CI are involved, it is an availability property.

A non-idempotent task reports changed on every run. Anything wired to that signal fires on every run. If what is wired to it is a service restart, you have built an outage generator that runs on a schedule and looks completely green.

The usual sources are worth being able to spot on sight:

The verification never changes: converge, run again, require changed=0. Put it in CI, not just in your own habits, so idempotency is enforced rather than remembered.

Analogy: A thermostat that fires the boiler every time you glance at it is not merely inefficient - it wears out the boiler, and the room was already the right temperature. The fault is in the sensor, and the sensor is your changed reporting.

A worked example

# Sources of a nightly restart storm, and their fixes

# 1. Command with no result semantics
- ansible.builtin.command: /opt/bin/refresh.sh
  register: r
  changed_when: "'UPDATED' in r.stdout"     # <- the fix

# 2. touch always reports changed
- ansible.builtin.file:
    path: /var/run/app.marker
    state: touch
    modification_time: preserve               # <- the fix
    access_time: preserve

# 3. Re-downloading every run
- ansible.builtin.get_url:
    url: https://example.test/app.tar.gz
    dest: /opt/app.tar.gz
    checksum: "sha256:abc123..."              # <- the fix

# 4. Re-extracting every run
- ansible.builtin.unarchive:
    src: /opt/app.tar.gz
    dest: /opt/app
    remote_src: true
    creates: /opt/app/VERSION                 # <- the fix

# The enforcement, in CI:
#   ansible-playbook site.yml
#   ansible-playbook site.yml | grep -q 'changed=0.*failed=0'

A subtlety worth naming: some genuine changes are not idempotency bugs. If two tools both manage a file's mode, copy will correctly report changed on every run because the mode really is different each time it looks. The fix is to establish single ownership of that file, not to silence the report with changed_when: false - which would hide a real, ongoing conflict.

Distinguishing "my task is lying" from "the system really is flapping" is the diagnostic skill here, and --diff answers it directly: it shows you what is actually different each run.

Warning: changed_when: false on a task that genuinely mutates the host does not fix an idempotency bug - it hides one, and it hides it from --diff, from handlers and from anyone reading the output.
Goal: Put this to work in the ansible-command-changed-when lab. Open /labs/ansible, pick ansible-command-changed-when, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.