Ansible · lesson 3 of 6
Roles and Handlers: notify Only Fires on changed, Never on ok
ansible-galaxy init scaffolds a real role directory. Wire a handler to a config-writing task with notify, and watch it disappear from the run entirely the moment the task reports ok instead of changed.
By SkillByExample · Updated
A role is just a standard directory layout — and the handler pattern inside it has one rule
that’s easy to assume wrong: notify only fires on changed, never on ok.
Scaffolding a real role
ansible-galaxy init roles/webserver
- Role roles/webserver was created successfully
find roles/webserver -type f | sort
roles/webserver/README.md
roles/webserver/defaults/main.yml
roles/webserver/handlers/main.yml
roles/webserver/meta/main.yml
roles/webserver/tasks/main.yml
roles/webserver/tests/inventory
roles/webserver/tests/test.yml
roles/webserver/vars/main.yml
ansible-galaxy init doesn’t ask what the role should do — it just lays out the directories
every role is expected to have, mostly empty, ready to fill in.
Wiring a task to a handler
# roles/webserver/defaults/main.yml
---
webserver_port: 8080
# roles/webserver/templates/config.j2
port={{ webserver_port }}
# roles/webserver/tasks/main.yml
---
- name: Create config directory
file:
path: /tmp/webserver-config
state: directory
- name: Write config file
template:
src: config.j2
dest: /tmp/webserver-config/app.conf
notify: Reload config
# roles/webserver/handlers/main.yml
---
- name: Reload config
debug:
msg: "Config reloaded on port {{ webserver_port }}"
# role_playbook.yml
---
- name: Use the webserver role
hosts: local
roles:
- webserver
ansible-playbook -i inventory.ini role_playbook.yml
TASK [webserver : Create config directory] *************************************
changed: [localhost]
TASK [webserver : Write config file] *******************************************
changed: [localhost]
RUNNING HANDLER [webserver : Reload config] ************************************
ok: [localhost] => {
"msg": "Config reloaded on port 8080"
}
PLAY RECAP *********************************************************************
localhost : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The config file was written for the first time (changed), which queued Reload config, and
it ran once, after both regular tasks completed — handlers always run at the end of the play,
not the instant they’re notified.
The behavior worth knowing: no change, no handler
ansible-playbook -i inventory.ini role_playbook.yml
TASK [webserver : Create config directory] *************************************
ok: [localhost]
TASK [webserver : Write config file] *******************************************
ok: [localhost]
PLAY RECAP *********************************************************************
localhost : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
No RUNNING HANDLER line at all this time — it doesn’t even appear in the output. ok=3, not
4: the handler genuinely didn’t run. Both tasks reported ok (the config directory already
existed, the rendered template already matched the file on disk), so nothing notified the
handler — and notify never fires on ok, only on changed. This is the exact behavior a
config-reload pattern depends on: reload only when the config actually changed, never on every
single run regardless of state.
Practice
1. Change webserver_port and confirm the handler fires again with the new value.
ansible-playbook -i inventory.ini role_playbook.yml -e webserver_port=9090
TASK [webserver : Write config file] *******************************************
changed: [localhost]
RUNNING HANDLER [webserver : Reload config] ************************************
ok: [localhost] => {
"msg": "Config reloaded on port 9090"
}
Changing the port changes what the template renders, so the file’s content no longer matches
what’s on disk — changed, which notifies the handler again, which prints the actual new
value it was given via -e.
2. Add a second task that also notifies Reload config, and confirm the handler still only runs once.
# roles/webserver/tasks/main.yml, second task added
- name: Touch a marker file
file:
path: /tmp/webserver-config/.reload-marker
state: touch
notify: Reload config
rm -rf /tmp/webserver-config
ansible-playbook -i inventory.ini role_playbook.yml
TASK [webserver : Create config directory] *************************************
changed: [localhost]
TASK [webserver : Write config file] *******************************************
changed: [localhost]
TASK [webserver : Touch a marker file] *****************************************
changed: [localhost]
RUNNING HANDLER [webserver : Reload config] ************************************
ok: [localhost] => {
"msg": "Config reloaded on port 8080"
}
PLAY RECAP *********************************************************************
localhost : ok=5 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
rm -rf wiped the whole directory, so all three tasks report changed this time — but still
only one RUNNING HANDLER line, not two, even though both Write config file and
Touch a marker file notify the same handler. Ansible deduplicates handlers notified multiple
times within a single play and runs each one exactly once, after every regular task has
finished.
3. List every variable a role's defaults/main.yml currently sets, without running the role.
cat roles/webserver/defaults/main.yml
---
webserver_port: 8080
Reading defaults/main.yml directly is often faster than digging through ansible-doc output
for a role you wrote yourself — it’s meant to be the readable, easily-overridden baseline for
exactly this kind of quick check.
Next: advanced playbook patterns — loops, conditionals, and error handling with blocks.