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.
- A handler should have a globally unique name within the playbook.
- If multiple handlers are defined with the same name, only the first one will be called. The remaining handlers will be ignored.
- Handlers always run in the order in which they are defined in the
handlerssection, and not in the
- 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
handlers directives are used. The
notify directive triggers the execution of the task(s) specified in the
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.
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.
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.
- Installing the latest version of Apache.
- Configuring Apache
- Starting Apache Service
- 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
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
--- - 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"
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
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
- Installation of EPEL package ( Extra Package for Enterprise Linux)
- Installation of Nginx web server
- 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
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.
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
--- - 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.
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.
ignored flag section at the very bottom of the output displays the number of tasks that have been skipped due to failures or errors.
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.