AnsibleXXII · Roles and ReuseRoles and reuse
Handlers across role boundaries
What you'll learn
- State why role handlers are not scoped to their role
- Predict which handler fires when two roles define the same name
- Use listen topics and name prefixes as cross-role interfaces
- Audit an existing repository for handler collisions
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
Everything else about a role is scoped. Its defaults lose to the inventory,
its vars are entered and left, its templates/ directory only affects
lookups inside it.
Handlers are not. roles/webapp/handlers/main.yml is loaded into the
parent play’s handler list, alongside the play’s own handlers and every
other role’s. One flat namespace, keyed by name, with no role prefix and no
warning on collision.
Two roles that each define a handler called restart nginx are two
definitions of one name. Notifying it runs one of them. Not necessarily
yours.
The collision, executed
Two roles. ra changes a config file and notifies restart nginx; its own
handlers file defines that name. rb also defines a handler called restart nginx — a different one, restarting a different unit — and does nothing
else in this play.
# roles/ra/tasks/main.yml
- name: role A changes config
ansible.builtin.debug: {msg: A-config}
changed_when: true
notify: restart nginx
# roles/ra/handlers/main.yml
- name: restart nginx
ansible.builtin.debug:
msg: "ROLE-A handler ran"
# roles/rb/handlers/main.yml
- name: restart nginx
ansible.builtin.debug:
msg: "ROLE-B handler ran"
- hosts: web
roles:
- ra
- rb
$ ansible-playbook -i inventory.ini collide.ymlTASK [ra : role A changes config] **********************************************
"msg": "A-config"
TASK [rb : role B does nothing interesting] ************************************
"msg": "B-noop"
RUNNING HANDLER [rb : restart nginx] *******************************************
"msg": "ROLE-B handler ran"
PLAY RECAP *********************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Read the handler line: RUNNING HANDLER [rb : restart nginx]. Role A’s task
notified, role B’s handler ran, and role A’s handler never executed at all.
The run reported success.
In production, with real service modules behind those names, that is a config change to service A followed by a restart of service B. Service A keeps serving its old configuration — so the change appears to have been applied and did nothing — and service B takes an unexplained restart during a maintenance window nobody scheduled for it.
Which one wins
The later definition replaces the earlier one for that name. Roles are loaded in the order the play lists them, so the last role to define a name is the one that gets notified — regardless of which role did the notifying.
That ordering dependence is the part that makes this hard to catch. Reorder
the roles: list for an unrelated reason and the behaviour changes. The
review that approved the reorder saw two role names swap places, not a
different service restarting.
Fix one: listen topics
listen: gives a handler an additional name that is explicitly a topic —
something other content notifies without knowing which handler answers.
# roles/ra/handlers/main.yml
- name: restart nginx
ansible.builtin.debug:
msg: "ROLE-A handler ran"
listen: reload web tier
# roles/ra/tasks/main.yml
- name: role A changes config
ansible.builtin.debug: {msg: A-config}
changed_when: true
notify: reload web tier
$ ansible-playbook -i inventory.ini collide.ymlRUNNING HANDLER [ra : restart nginx] *******************************************
"msg": "ROLE-A handler ran"
PLAY RECAP *********************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0Topics are many-to-many by design: several handlers may listen to one topic
and all of them run. That makes listen the right interface when a change
should trigger work in more than one role — “the certificate changed” ought to
reload nginx and haproxy, and neither role should have to know the other
exists.
It is the weaker fix for the collision itself, though, because the colliding names are still there for the next person to notify by accident.
Fix two: namespace the names
The fix that removes the problem rather than routing around it is the naming discipline from Part XIII lesson 7, applied to handlers:
# roles/ra/handlers/main.yml
- name: ra restart nginx
ansible.builtin.debug:
msg: "ROLE-A handler ran"
$ ansible-playbook -i inventory.ini collide.ymlRUNNING HANDLER [ra : ra restart nginx] ****************************************
"msg": "ROLE-A handler ran"
PLAY RECAP *********************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0The line reads redundantly — ra : ra restart nginx — and that redundancy is
the price of a name that cannot be claimed by somebody else’s role. Use the
role name as the prefix and the redundancy is at least predictable.
Use both. Prefix every handler name so nothing can collide, and add a
listen: topic for the handlers that other roles legitimately need to
trigger. The prefix protects the implementation; the topic is the interface.
Auditing an estate you inherited
Collisions are cheap to find and worth finding before an incident does it for you:
grep -rhoP '^-\s+name:\s+\K.*' roles/*/handlers/*.yml \
| sed 's/[[:space:]]*$//' \
| sort | uniq -dAnything printed is a name two or more roles define. For each one, decide which role owns it, prefix the others, and re-run.
Then check the notify side, because a handler defined once can still be notified from a role that did not mean this one:
grep -rn 'notify:' roles/*/tasks/ | sed 's|roles/||'Knowledge check
Knowledge check · 4 questions
Q1. Two roles in one play each define a handler named restart nginx. A task in the first role notifies that name. What happens?
Q2. Which changes remove or route around a cross-role handler name collision? Select all that apply.
Q3. A notify referring to a handler name that no handler defines fails the run, so typos in notify are loud.
Q4. An operator reports that a config change is not taking effect, and checks that the task reports changed and that a RUNNING HANDLER block appears in the output. Both are true. What should they look at next?
Passing score: 75%. Answers are checked in this browser.