Handlers Firing Every Run and a Conditional That Never Triggers
A playbook manages 30 app servers. It runs command: /usr/bin/appctl status registered as appstat, then a debug task gated by when: appstat.stdout > 100 that never fires even though the reported value is 250 on several hosts. A second task, shell: grep -c worker /etc/app/app.conf, always reports changed, so the restart app handler fires on every run, producing 30 restarts per deploy. A template task inside a block guarded by when: ansible_facts['distribution_major_version'] == '9' notifies the handler on only some hosts. The team requires: no restart unless configuration content actually changed, a failing config validation must abort before any restart, and a hard failure when the worker count is below 100.
1. Why does `when: appstat.stdout > 100` not do what the author intended, and how do you turn the worker-count requirement into a hard failure?
Read the worked answer
Registered stdout is always a string. Comparing it to the integer 100 is a type mismatch, so the conditional raises a templating error rather than quietly evaluating to false - and a run that resolves the value as 250 yet never fires usually means the error is being masked or the value carries whitespace/newline. The fix is a cast plus an explicit type check, because | int silently returns 0 for non-numeric input and 0 will also fail the threshold for the wrong reason:
- name: Read worker count
ansible.builtin.command: /usr/bin/appctl status
register: appstat
changed_when: false
- name: Validate worker count
ansible.builtin.assert:
that:
- appstat.stdout | trim is match('^[0-9]+$')
- appstat.stdout | int >= 100
fail_msg: "worker count '{{ appstat.stdout | trim }}' invalid on {{ inventory_hostname }}"
Using assert rather than when makes the requirement fail the play loudly. changed_when: false is mandatory on a status command, otherwise it is permanently changed and notifies handlers.
2. Why does the `restart app` handler fire on every run, and how do you make restart conditional on real content change?
Read the worked answer
shell and command have no way to know the remote state; Ansible marks them changed whenever they run and return success. grep -c also exits non-zero when the count is 0, so it can fail outright. Hence a notify is triggered every deploy.
Ranked fixes:
- Replace the read with a state-aware module.
ansible.builtin.slurpplus anassert, oransible.builtin.stat, never report changed. - Make the edit idempotent with
ansible.builtin.templateorlineinfileinstead of shelling out.templatereports changed only when rendered content differs, which is exactly the notify condition wanted. - If the shell read must stay:
- name: Count workers
ansible.builtin.command: grep -c worker /etc/app/app.conf
register: workers
changed_when: false
failed_when: false
Then gate the restart explicitly with notify on the template task only, never on the read. Add validate: '/usr/bin/appctl validate %s' to the template task so a syntactically invalid file is rejected before it is written - validation failure then aborts the task, so the handler is not notified and the running service keeps its last good config.
3. The handler is notified only on some hosts for an identical template task. What explains that, and how do you guarantee the restart happens before later tasks run?
Read the worked answer
Two separate effects, both expected Ansible behaviour:
- The
block-levelwhenevaluates per host. On hosts that are not RHEL 9 the task is skipped, andskippedis notchanged, so no notify. That is correct - but it means the group is heterogeneous, so verifydistribution_major_versionagainst the expected inventory rather than assuming all 30 are RHEL 9. - On hosts where the condition is true, the task notifies only if the rendered result differs from the file on disk. Hosts already carrying the target content legitimately do not restart. Confirm with
--check --diffbefore concluding the logic is broken.
Ordering: handlers run at the end of the play by default, so later tasks run against the old config. Insert meta: flush_handlers immediately after the validation and template tasks so the restart is applied before dependent tasks execute:
- Keep the validate task
changed_when: false, failed_when: rc != 0so validation failure aborts first. - If a host fails after notifying, its handlers are discarded unless
force_handlers: trueis set. For a rolling deploy, also useserial: 5andmax_fail_percentageso the restart boundary is controllable per batch.