You are currently viewing How To Work With Handlers In Ansible With Examples

How To Work With Handlers In Ansible With Examples

The handler is a built-in ansible feature that offers controlled task execution. The tasks defined under handlers are called handler tasks and they run only when notified to run. 

Let’s say you installed nginix in one of the managed hosts and made some configuration changes. For the changes to be effective you have to restart the nginix service. You can create a handler task to restart the service. Here the handler task will only be triggered if there are any changes to the configuration.

Ansible Handlers Syntax Representation

The handler task is comprised of two directives.

  1. Notify – Think of this directive as a way to tell the handler to run the task. Notify directive will send a signal to the handler task.
  2. Handler – All the tasks are grouped under this directive.

Take a look at the below playbook snippet. It contains two tasks where the first task creates a file in the home directory and the second task under the handlers section removes the file. 

In the first task notify directive is added. If you look at the name passed to the notify directive and handler task name both will be the same. Notify directive sends a signal using the task name. So keep the task name short and descriptive.

NOTE: Handler task will only run when the caller task returns “changed: True”.
---
- name: Handler Test
  hosts: localhost
  gather_facts: false

  vars:
    USER: "{{ lookup('ansible.builtin.env', 'USER') }}"
    PATH: "/home/{{ USER }}/samplefile.txt"

  tasks:
    - name: Creating a empty file
      ansible.builtin.file: 
        path: "{{ PATH }}"
        state: touch
      notify: remove_file

  handlers:
    - name: remove_file
      ansible.builtin.file:
        path: "{{ PATH }}"
        state: absent

When I run this playbook, the first task creates an empty file and set the task status as changed. The first task then sends a signal to run the handler task.

TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]

RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]

Ansible Handlers Order Of Execution

Handler tasks will get executed only after all other regular tasks are completed. To demonstrate this I have added one more regular task which will change the file permission.

tasks:
  - name: Creating a empty file
    ansible.builtin.file: 
      path: "{{ PATH }}"
      state: touch
    notify: remove_file

  - name: Changing file permission
    ansible.builtin.file:
      path: "{{ PATH }}"
      mode: "0700"

handlers:
  - name: remove_file
    ansible.builtin.file:
      path: "{{ PATH }}"
      state: absent

If you see the below output

  1. The first task ran fine which creates an empty file.
  2. The second task ran after the first task instead of the handler task.
  3. The handler task ran as the final task.
TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]

TASK [Changing file permission] ******************************************************************************************
changed: [localhost]

RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]

If you wish to run the handler task immediately after the parent task you can use the built-in meta module. In the following playbook snippet, the second task is using the meta module with “flush_handler”.
The third task will fail because the handler task will run and delete the file before the third task. I have added the ignore_errors directive to ignore the failure.

tasks:
  - name: Creating a empty file
    ansible.builtin.file: 
      path: "{{ PATH }}"
      state: touch
    notify: remove_file

  - name: Flush handlers and run task
    ansible.builtin.meta: flush_handlers

  - name: Changing file permission
    ansible.builtin.file:
      path: "{{ PATH }}"
      mode: "0700"
    ignore_errors: True

handlers:
  - name: remove_file
    ansible.builtin.file:
      path: "{{ PATH }}"
      state: absent

In the below output you can see after the first task, the flush hander task is submitted followed by the handler task.

TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]

TASK [Flush handlers and run task] ***************************************************************************************

RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]

TASK [Changing file permission] ******************************************************************************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "file (/home/ansuser/samplefile.txt) is absent, cannot continue", "path": "/home/ansuser/samplefile.txt", "state": "absent"}
...ignoring

Ansible Handlers Calling Multiple Handler Tasks

Sometimes you may want to run multiple handler tasks after a regular task gets completed. This is possible by passing multiple handler task names to the notify directive. 

In the following playbook snippet, a new handler task is added which will just print a message to stdout. if you look at the notify directive both the task names are passed in the python list format notation.

tasks:
  - name: Creating a empty file
    ansible.builtin.file: 
      path: "{{ PATH }}"
      state: touch
    notify: ["remove_file", "final_task"]

handlers:
  - name: remove_file
    ansible.builtin.file:
      path: "{{ PATH }}"
      state: absent

  - name: final_task
    ansible.builtin.debug:
      msg: "This is the final task"

There are two supported formats to pass a list of task names to notify the directive. You can use yaml notation or python notation.

# yaml notation
notify:
  - remove_file
  - final_task

# python notation
notify: ["remove_file", "final_task"]

Ansible Handlers Duplicate Tasks

When you create two handler task with the same name, ansible will only consider the first interpreted task and executes it ignoring the subsequent duplicate task.

I am running the same playbook from the previous section but changed both the handler task name to “final_tasks”. In the below output, you can see only the file removal task ran but not the task that prints the message to stdout.

PLAY [Handler Test] ******************************************************************************************************

TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]

RUNNING HANDLER [remove_file] ********************************************************************************************
changed: [localhost]
NOTE: Always give descriptive and meaningful names to the tasks to eliminate duplication issue.

Ansible Handlers – Handling Failures

If a task gets failed in a host, the handler task for that particular host will not run even though notify signal is already sent. There are a couple of options to handle failures.

1. Use the meta module to run the handler task after the parent task. We have already seen the meta module in the previous section.

2. Use “ignore_errors: yes” which will ignore the failure and run the handler task. To know more about the ignore_errors directive, look at the following article.

3. Set the property “force_handler: true” which will run the handler task irrespective of failure. I am using the same playbook but the second task is set to fail purposely.

---
- name: Handler Test
  hosts: localhost
  gather_facts: false
  force_handlers: true

  vars:
    USER: "{{ lookup('ansible.builtin.env', 'USER')|default('nobody', True) }}"
    PATH: "/home/{{ USER }}/samplefile.txt"

  tasks:
    - name: Creating a empty file
      ansible.builtin.file: 
        path: "{{ PATH }}"
        state: touch
      notify: final_task
    
    - name: Task to be failed
      ansible.builtin.shell: /bin/false
      ignore_errors: True

  handlers:
    - name: final_task
      ansible.builtin.debug:
        msg: "Running final task."

In the below output you can see the handler task ran fine even though the second task got failed.

PLAY [Handler Test] ******************************************************************************************************

TASK [Creating a empty file] *********************************************************************************************
changed: [localhost]

TASK [Task to be failed] *************************************************************************************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": "/bin/false", "delta": "0:00:00.003097", "end": "2023-02-06 18:54:12.060197", "msg": "non-zero return code", "rc": 1, "start": "2023-02-06 18:54:12.057100", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
...ignoring

RUNNING HANDLER [final_task] *********************************************************************************************
ok: [localhost] => {
    "msg": "Running final task."
}

You can also set “force_handler = true” in ansible.cfg file or pass –force-handlers flag when running the ansible-playbook command.

Run Group Of Handler Tasks Using Listen Directive

The notify directive accepts the task name as input. Alternatively, you can use the “listen” directive and pass the name to the notify directive. The main advantage of the listen directive is task grouping.

Take a look at the below handler task where the listen directive is set to “run all”. The same value is passed to notify directive which sends the signal to run both the handler tasks. 

Instead of passing different task names to a single notify directive, listen directive groups multiple tasks, and only one value is passed to the notify directive.

tasks:
  - name: Creating a empty file
    ansible.builtin.file: 
      path: "{{ PATH }}"
      state: touch
    notify: "run all"

handlers:
  - name: first handler task
    ansible.builtin.debug:
      msg: "processing some data from file"
    listen: "run all"

  - name: second handler task
    ansible.builtin.file:
      path: "{{ PATH }}"
      state: absent
    listen: "run all"

Wrap Up

In this article, we have seen what notify directive is and its usage. We have also seen how handlers behave in different cases.  Finally, we have also seen how to group handler tasks using the listen directive.

Leave a Reply

3 × 2 =