How to implement and use handlers in Ansible code?

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!

Leave a comment