AnsibleLII · Anti-PatternsAnti-patterns of reporting
Anti-pattern: tasks that always change, or never do
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
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
- name: Regenerate the application configuration
ansible.builtin.command:
cmd: /srv/app/bin/generate-config
notify: Restart app$ ansible-playbook -i inventory/hosts.yml shellish.ymlTASK [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=0The 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=300every night meanschanged=300carries 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_whenin a role three levels deep.
Direction 2: never changed
- name: Write the application configuration
ansible.builtin.command:
cmd: /srv/app/bin/write-config --output /etc/app/app.conf
changed_when: false
notify: Restart appThis 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 tool | Anti-pattern | |
|---|---|---|
| Value | Derived from the command’s own evidence | Hard-coded false |
| Effect | Reporting matches what happened | Reporting contradicts what happened |
| Test | If the task changed something, this would say so | It would not |
# 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 appThe 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
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- 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.
- 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.
- Run the play twice on a canary and treat any changed on the second run as a finding to explain.
- 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.
- Add ansible-lint no-changed-when to CI so the count cannot grow again silently.
Knowledge check
Knowledge check · 4 questions
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?
Q2. Which failures follow from a task that rewrites a config file but is hard-coded to changed_when: false? Select all that apply.
Q3. changed_when is evaluated after the module has run, so it can never make a task idempotent - only change what the task reports.
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.