Handlers are Ansible’s solution for running specific operations only when other tasks made changes, like when we update the configuration of a web server, and we want to restart the service. Obviously we do not want to restart the Apache2 on every playbook run, only if there is a change in its configuration. This is why we use handlers.
Handlers are easy to implement, and they are convenient to use. In a playbook we use the handlers
keyword under the tasks
. In a role
directory structure we have an own directory for the handlers. Ansible handlers will run only after every task finished their operations by default.
Handlers look like ordinary tasks, and we can “notify” them directly from other tasks.
Let’s see a basic handler in action in our test Ansible playbook!
Running handlers in playbooks
---
- name: Test playbook for playing with Ansible
hosts: all
tasks:
- name: The first task
ansible.builtin.debug:
msg: "Starting the work with tasks"
notify: First handler
handlers:
- name: First handler
ansible.builtin.debug:
msg: "I was notified by a task"
Above it is an example of a task notifying a handler in an Ansible playbook. This is the most common structure I have seen all over the internet. The handler called “First handler” will not be executed when we run the playbook because handlers only get triggered by the task change. In our case the ansible.builtin.debug was not registered as a change, thus the handler was not triggered.
Let’s force a task change and see the handler in operation.
- name: The first task
ansible.builtin.debug:
msg: "Starting the work with tasks"
changed_when: true
notify: First handler
Now we can see our handler in operation.
$ ansible-playbook -i inventory play-test.yml --limit ansible-control
PLAY [Test playbook for playing with Ansible] ******************************************************************************************************************
TASK [Gathering Facts] *****************************************************************************************************************************************
ok: [ansible-control]
TASK [The first task] ******************************************************************************************************************************************
changed: [ansible-control] => {
"msg": "Starting the work with tasks"
}
RUNNING HANDLER [First handler] ********************************************************************************************************************************
ok: [ansible-control] => {
"msg": "I was notified by a task"
}
PLAY RECAP *****************************************************************************************************************************************************
ansible-control : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Here we are, the “RUNNING HANDLER” part tells us that our handler worked this time. It ran exactly after all the tasks finished.
Running multiple handlers from a task
A task can notify multiple handlers if necessary. In this case we have to use a list YAML structure in notify
.
- name: The first task
ansible.builtin.debug:
msg: "Starting the work with tasks"
changed_when: true
notify:
- First handler
- Second handler
Running multiple handlers with the listen keyword
There is an alternate way of notifying multiple handlers from a task. In this case we have to use the listen
keyword in our handlers.
---
- name: Test playbook for playing with Ansible
hosts: all
tasks:
- name: The first task
ansible.builtin.debug:
msg: "Starting the work with tasks"
changed_when: true
notify: Restart the services
handlers:
- name: First handler
ansible.builtin.debug:
msg: "I was notified by a task"
listen: Restart the services
- name: Second handler
ansible.builtin.debug:
msg: "I was notified by a task"
listen: Restart the services
In this particular case we use a string in the notify
part (and not the name of the handler) that describes well what is it for. The handlers
part listens on this operation with the listen
keyword. Note that there are multiple handlers listening on the same notify
.
In roles we have to place our handlers into the handlers/main.yml
file. Everything else works the same way as we reviewed above.
Controlling when the handlers run
By default handlers will run when every task finished their work in a playbook. We can control the running of handlers if we want to trigger them before the tasks finished.
In this case we use the meta module of Ansible to flush those handlers.
---
- name: Test playbook for playing with Ansible
hosts: all
tasks:
- name: The first task
ansible.builtin.debug:
msg: "Starting the work with tasks"
changed_when: true
notify: Restart the services
- name: Flush those handlers
ansible.builtin.meta: flush_handlers
- name: The second task
ansible.builtin.debug:
msg: "Starting the work with tasks"
changed_when: true
notify: Restart the services
handlers:
- name: First handler
ansible.builtin.debug:
msg: "I was notified by a task"
listen: Restart the services
- name: Second handler
ansible.builtin.debug:
msg: "I was notified by a task"
listen: Restart the services
The above code will trigger the handler run after the first task.
$ ansible-playbook play-test.yml --limit ansible-control
PLAY [Test playbook for playing with Ansible] ******************************************************************************************************************
TASK [Gathering Facts] *****************************************************************************************************************************************
ok: [ansible-control]
TASK [The first task] ******************************************************************************************************************************************
changed: [ansible-control] => {
"msg": "Starting the work with tasks"
}
TASK [Flush those handlers] ************************************************************************************************************************************
RUNNING HANDLER [First handler] ********************************************************************************************************************************
ok: [ansible-control] => {
"msg": "I was notified by a task"
}
RUNNING HANDLER [Second handler] *******************************************************************************************************************************
ok: [ansible-control] => {
"msg": "I was notified by a task"
}
TASK [The second task] *****************************************************************************************************************************************
changed: [ansible-control] => {
"msg": "Starting the work with tasks"
}
RUNNING HANDLER [First handler] ********************************************************************************************************************************
ok: [ansible-control] => {
"msg": "I was notified by a task"
}
RUNNING HANDLER [Second handler] *******************************************************************************************************************************
ok: [ansible-control] => {
"msg": "I was notified by a task"
}
PLAY RECAP *****************************************************************************************************************************************************
ansible-control : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
We can see that handlers are a comfortable and secure way to trigger actions based on task changes. They are helpful when we want to write idempotent code.
If you have anything to share then please visit my Tom’s IT Cafe Discord Server!