Definition

A handler is a type of task in Ansible that is intended to be executed after being called by other tasks

The main differences between a task and a handler:

  1. Handlers are executed at the very end of a group of tasks and can be called up to 3 times (see the previous post about pre_tasks/post_tasks), while the order of execution will correspond to the order of recording
  2. Handlers are called by the notify: <handler_name> directive and are executed only if the task result has the CHANGED status
  3. The handler will be executed once, even if it was called several times

According to the official documentation, handlers should be used to restart or reboot services, but their scope of application does not end there; in general, the use of a handler looks like this:

tasks:
  - name: Configure network
    ansible.builtin.template:
      dest: /etc/network/interfaces
      src: networking.j2
      owner: root
      group: root
      mode: "0644"
      backup: true
    notify: Restart networking

handlers:
  - name: Restart networking
    ansible.builtin.systemd:
      name: networking
      state: restarted

Practice

Logically, the task below is equivalent to the task above, but using when: <some_var>.changed is very bad ansible practice:

tasks:
  - name: Configure network
    ansible.builtin.template:
      dest: /etc/network/interfaces
      src: networking.j2
      owner: root
      group: root
      mode: "0644"
      backup: true
    register: network_config

  - name: Restart networking
    ansible.builtin.systemd:
      name: networking       
      state: restarted
    when: network_config.changed

But that’s not all, handlers can do much more, for example:

A handler can call other handlers or even a group of tasks via include_tasks or import_tasks:

tasks:
  - name: Always changed
    ansible.builtin.command: /bin/true
    notify: 
      - First handler

handlers:
  - name: First handler
    ansible.builtin.command: /bin/true
    notify: Inlcude tasks

  - name: Inlcude tasks
    ansible.builtin.include_tasks: tasks/main.

You can change the name to call the handler by using a Jinja2 pattern in the handler name, or by using the listen: directive, but only static strings can be used for listen::

vars:
  service: apache2
tasks:
  - name: Always changed
    ansible.builtin.command: /bin/true
    notify: 
      - Restart {{ service }}
      - Reboot

handlers:
  - name: Restart {{ service }}
    ansible.builtin.systemd:
      name: "{{ service }}"
      state: restarted

  - name: Reboot task
    ansible.builtin.reboot:
    listen: Reboot

Using the ansible.builtin.meta:flush_handlers module, you can execute all previously called handlers and reset their counter, allowing you to call them again:

tasks:
  - name: Configure network
    ansible.builtin.template:
      dest: /etc/network/interfaces
      src: networking.j2
      owner: root
      group: root
      mode: "0644"
      backup: true
    notify: Restart networking

  - name: Restart right away
    ansible.builtin.meta: flush_handlers

  - name: Notify again
    ansible.builtin.command: /bin/true
    notify: Restart networking

handlers:
  - name: Restart networking
    ansible.builtin.systemd:
      name: networking
      state: restarted

Since roles is simply “included” in tasks, it is possible to call handlers from a role (an example can also be found in the previous post), and for convenience, it is possible to use the role_name/FQCN_role : handler name construction, for example:

- name: Always change
  ansible.builtin.command: /bin/true
  notify: 'k3s.orchestration.raspberrypi : Reboot Pi'

Based on point 3, the only condition for calling the handler is the CHANGED status, and, as a consequence, you can define the change condition yourself by changed_when: <condition>:

tasks:
  - name: Run sample task
    ansible.builtin.command: /bin/true
    changed_when: false
    notify: Say hello

# Will not be called due to 'changed_when: false'
handlers:
  - name: Say hello
    ansible.builtin.debug:
      msg: Hello guys