Skip to main content
RunBook Academy

AnsibleLII · Anti-PatternsAnti-patterns of reporting

Anti-pattern: tasks that always change, or never do

Intermediate⏱ ~26 minbash

What you'll learn

  • Diagnose a nightly restart storm back to a single task changed result
  • Explain why a never-changed task is the more dangerous of the two directions
  • Distinguish blanket changed_when: false from the precision use of the same keyword
  • Audit a converged fleet for tasks that report a change on every run

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.

changed is a single boolean, and an unusual amount of this course rests on it. Handlers fire on it. The recap counts it. Drift detection reads it. The audit trail your compliance team looks at is a summary of it.

The anti-pattern is a changed result that does not correspond to a change. It has two directions and they fail completely differently.

Direction 1: always changed

Service impact possiblea task that reports a change on every run
- name: Regenerate the application configuration
ansible.builtin.command:
  cmd: /srv/app/bin/generate-config
notify: Restart app
Read-only / Safewhat command reports, with and without changed_when
$ ansible-playbook -i inventory/hosts.yml shellish.yml
TASK [Query the effective user] ************************************************
ok: [localhost]

TASK [The same query with no changed_when] *************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

The failure. The handler fires on every run. On a nightly play across three hundred hosts, that is three hundred service restarts a night on a fleet where nothing changed.

The consequences compound:

  • User-visible impact. Every restart drops connections, empties caches and produces a latency spike. Nightly, forever.
  • The recap becomes noise. changed=300 every night means changed=300 carries no information, so the night something genuinely changes is indistinguishable.
  • Diagnosis is hard out of proportion to the cause. The symptom is “the application restarts at 02:00 and nobody knows why”. The investigation starts with systemd, the package manager and cron. The cause is one missing changed_when in a role three levels deep.

Direction 2: never changed

Service impact possiblethe blanket suppression
- name: Write the application configuration
ansible.builtin.command:
  cmd: /srv/app/bin/write-config --output /etc/app/app.conf
changed_when: false
notify: Restart app

This gets added for an understandable reason: the recap was noisy, and changed_when: false made it quiet. It is direction 1 apparently fixed, and it is much worse.

The failure, part one: the handler never fires. notify acts only on a genuine changed result. The configuration file is rewritten and the service is never restarted, so it keeps running the old configuration. The repository, the recap and the drift report all agree that the host is converged. The host disagrees, and nothing asks it.

The failure, part two: drift becomes invisible. Part XXXVI’s drift detection is a check-mode run whose changed results are the drift report. A task hard-coded to changed_when: false reports no drift whatever the host’s state — so the one mechanism designed to catch a host that has quietly diverged is blind on exactly that task.

The line between the tool and the anti-pattern

changed_when is taught in earnest in Part XII, and it is the right answer to a real problem. The distinction is exact:

Precision toolAnti-pattern
ValueDerived from the command’s own evidenceHard-coded false
EffectReporting matches what happenedReporting contradicts what happened
TestIf the task changed something, this would say soIt would not
Configuration changethe same task, done three ways
# Honest: this task genuinely changes nothing, and says so.
- name: Read the current version
ansible.builtin.command:
  cmd: /srv/app/bin/version
register: app_version_out
changed_when: false
check_mode: false

# Honest: the answer is derived from the command's own output.
- name: Reload the ruleset if it differs
ansible.builtin.command:
  cmd: /usr/sbin/nft -c -f /etc/nftables.conf
register: nft_check
changed_when: nft_check.rc == 1
failed_when: nft_check.rc not in [0, 1]

# Better still: a module that knows what it manages.
- name: Write the application configuration
ansible.builtin.template:
  src: app.conf.j2
  dest: /etc/app/app.conf
  validate: '/srv/app/bin/check-config %s'
notify: Restart app

The third form is the one to reach for. template compares the rendered content against what is on the host and reports changed only when they differ — so the handler fires exactly when it should, --check shows what would change, and --diff shows the content. All three properties come from using a module that understands its subject, which is the same argument as lesson 1 of this part.

Auditing a fleet for it

Read-only / Safefind the suppressions and the always-changers
cd /srv/ansible

# Hard-coded suppressions, which need reviewing individually.
grep -rn 'changed_when:\s*\(false\|False\|no\)'    --include='*.yml' --include='*.yaml' .

# Files containing a command/shell task but no changed_when anywhere -
# candidates for direction 1, since those tasks report changed on every
# successful run.
grep -rlE '^\s*(ansible\.builtin\.)?(command|shell):'    --include='*.yml' . | xargs grep -L 'changed_when'

# ansible-lint names them directly, task by task.
ansible-lint --profile production

# The behavioural test: two runs, second should be changed=0.
ansible-playbook site.yml --limit canary
ansible-playbook site.yml --limit canary
  1. For every hard-coded changed_when: false, ask whether the task genuinely changes nothing. If it does change something, that is a defect, and the handler downstream of it is not firing.
  2. For every command/shell task with no changed_when, decide: is it a read, in which case changed_when: false is honest, or does it act, in which case derive the value from evidence.
  3. Run the play twice on a canary and treat any changed on the second run as a finding to explain.
  4. Check what each affected handler does. A restart storm on a stateless web tier is annoying; on a database or a message broker it is an outage.
  5. Add ansible-lint no-changed-when to CI so the count cannot grow again silently.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A converged 300-host fleet restarts its application every night at 02:00 and nobody knows why. What is the most likely cause, and how would you confirm it in two minutes?

  2. Q2. Which failures follow from a task that rewrites a config file but is hard-coded to changed_when: false? Select all that apply.

  3. Q3. changed_when is evaluated after the module has run, so it can never make a task idempotent - only change what the task reports.

  4. Q4. Which of these uses of changed_when is the precision tool rather than the anti-pattern?

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