How To Define and Use Handlers in Ansible Playbooks

July 27th, 2022
How To Define and Use Handlers in Ansible Playbooks

Any change of state or configuration of an application requires a restart for the changes to be applied. For example, you might want to restart Apache HTTP server after configuring a virtual host file for your domain. To accomplish this, you would need to specify two tasks in your playbook - one task to configure the virtual host file and the other to restart Apache.

But here’s a challenge. The Apache service will always be restarted with each successive playbook run regardless of whether or not there is a change in the configuration file. This is because there is a task that explicitly restarts Apache and does not take into consideration the state of the Apache service or the changes made in the configuration file. Restarting the service with each subsequent run is not desirable as it often leads to resource overhead. To address this issue, Ansible handlers are used.

In Ansible, handlers are just like any regular tasks. The difference between handlers and regular tasks is that handlers only run when 'notified' using the notify directive. Handlers are usually used to start, restart, reload and stop services on target nodes only when there is a change in the state of the task, and not when no change is made. This helps to achieve idempotency.

By default, handlers are executed last regardless of their location in the playbook. You can define and call one or more handlers in a playbook depending on the tasks to be carried out.

In this topic, you will learn how Ansible Handlers are used in playbooks.

Using Ansible Handlers

As earlier mentioned, handlers are just like other tasks in a playbook, the difference being that they are triggered using the notify directive, and are only run when there is a change of state.

To get the most out of using handlers, here are some points to keep in mind.

  1. A handler should have a globally unique name within the playbook.
  2. If multiple handlers are defined with the same name, only the first one will be called. The remaining handlers will be ignored.
  3. Handlers always run in the order in which they are defined in the handlers section, and not in the notify section.
  4. If the state of a task remains unchanged, the handler will not run. As such, handlers help in achieving idempotency. Hence a handler task only runs if there is a change in state, else it doesn’t.

To define a handler, the notify and handlers directives are used. The notify directive triggers the execution of the task(s) specified in the handlers section.

Let us now explore how to define and call Ansible handlers.

A single task and a handler

Consider the following playbook that installs and starts Apache on a target system.

---
- name: Install Apache on RHEL server
  hosts: webserver
  tasks:
    - name: Install the latest version of Apache
      dnf:
        name: httpd
        state: latest

      notify:
       - Start Apache

  handlers:
     - name: Start Apache
       service:
         name: httpd
         state: started

The playbook consists of a regular task and a handler. The regular task installs the Apache HTTP server on the target system. Once installed the notify directive calls the handler task which starts the Apache service.

During playbook runtime, the regular task is executed first followed by the handler.

Ansible task followed by a handler

If the playbook is executed again, the handler task will not run for the simple reason that both the state and configuration of the Apache service remain unchanged.

Ansible handlers do not run if state is unchanged

Multiple tasks and handlers

In most cases, you will be dealing with multiple tasks which might require multiple handlers.

The following playbook contains two regular tasks and two handlers broken down as follows.

Regular tasks

  1. Installing the latest version of Apache.
  2. Configuring Apache

Handlers

  1. Starting Apache Service
  2. Configuring the firewall to allow inbound HTTP traffic
---
- name: Install Apache on RHEL server
  hosts: webserver
  tasks:
    - name: Install the latest version of Apache
      dnf:
        name: httpd
        state: latest

    - name: Configure Apache 
      copy:
        src: /home/cherry/Documents/index.html
        dest: /var/www/html
        owner: apache
        group: apache        
        mode: 0644

      notify:
      - Configure Firewall
      - Start Apache   


  handlers:

     - name: Start Apache
       service:
         name: httpd
         state: started

     - name: Configure Firewall
       firewalld:
         permanent: yes
         immediate: yes
         service: http
         state: enabled

The first task installs Apache while the second one configures Apache by making changes to the default index.html file and applying the required group, user ownership, and file permissions.

During playbook runtime, the 'Start Apache' handler is executed first followed by the 'Configure Firewall' handler despite the latter appearing first in the notify section. As mentioned earlier, the order of execution is determined by the handlers section and not the notify section.

The order of execution of Ansible handlers

Grouping Handlers Using the 'listen' Directive

Alternatively, you can group handlers using the listen keyword and call them using a single notify statement. In the following playbook, the notify directive is set to restart services which triggers all the handler tasks bearing the listen directive.

---
- name: 
  hosts: webserver
  tasks:
    - name: Restart services on remote target
      command: echo "This task restarts services"
      notify: "restart services"

  handlers:
    - name: Restart Apache
      service:
        name: httpd
        state: restarted
      listen: "restart services" 

    - name: verify rsyslog running service:
      service:
        name: rsyslog
        state: restarted
      listen: "restart services" 

Ansible listen directive for grouping handlers

Determine When Handlers Run

By default, handlers run at the end of the play once all the regular tasks have been executed. If you want handlers to run before the end of the play or between tasks, add a meta task to flush them using the meta module.
The meta task is defined as follows.

- name: Flush handlers
  meta: flush_handlers

The meta: flush_handlers task calls any handlers that have been notified at that point in the play.

The following playbook performs the following tasks in order

  1. Installation of EPEL package ( Extra Package for Enterprise Linux)
  2. Installation of Nginx web server
  3. Installation of Neofetch
---
- name: Control when handlers run using meta directive
  hosts: webserver

  tasks:

    - name: Add EPEL repository
      dnf:
        name: epel-release
        state: latest

    - name: Install Nginx web server
      dnf:
        name: nginx
        state: latest

      notify:
      - Start Nginx

    - name: Flush handlers
      meta: flush_handlers

    - name: Install Neofetch
      dnf:
        name: neofetch
        state: latest

  handlers:
    - name: Start Nginx
      service:
        name: nginx
        state: started

The meta: flush_handlers meta task triggers the Start Nginx handler to be executed at that point in the play. The remaining task follows suit.

From the output of the playbook runtime, you can see that the handler runs just before the last task.

Change Ansible handlers execution flow

Handling task errors

By default, when a task fails in a particular host, subsequent tasks are not executed on that host and the playbook exits with an error. Consequently, the handler tasks are also ignored and do not get executed.

Let’s take a simple playbook example to illustrate this. The following playbook contains two tasks. The first task defined by the shell module is set to fail using the /bin/false argument. The second task, however, is configured to run successfully using the /bin/false argument.

--- 
- name: Simulate a task to fail on a host
  hosts: webserver


  tasks:
    - name: set a task to fail
      shell: /bin/false

    - name: set a task to run successfully
      shell: /bin/true
      notify: success


  handlers:
    - name: success
      debug: 
        msg: "This task has been executed successfully"

As expected, the playbook executes and fails upon encountering an error in the first task.

Ansible playbook fails upon encountering an error

To ignore failed tasks, use the ignore_errors: yes property in the playbook as follows.

--- 
- name: Simulate a task to fail on a host
  hosts: webserver
  ignore_errors: yes

This time around, the failed task is ignored and the playbook proceeds to run the remaining tasks.

Ansible playbook continues after encountering an error

The ignored flag section at the very bottom of the output displays the number of tasks that have been skipped due to failures or errors.

Wrapping up

In this tutorial, we have explored what handlers are and how they are defined and called using the notify directive. We have also looked at how handlers can be used alongside multiple tasks and how to handle errors associated with task failure. Visit the Ansible Documentation for more information about Ansible handlers.

Helping engineers learn 💡 about new technologies and ingenious IT automation use cases to build better systems 💻

Join Cherry Servers Community

Get monthly practical guides about building more secure, efficient and easier to scale systems on an open cloud ecosystem.

We use cookies to ensure seamless user experience for our website. Required cookies - technical, functional and analytical - are set automatically. Please accept the use of targeted cookies to ensure the best marketing experience for your user journey. You may revoke your consent at any time through our Cookie Policy.