Ansible is a configuration management tool that helps maintain software configurations across infrastructure components. It uses an agent-less architecture to manage these changes.
At the core of any Ansible project, playbooks outline a series of steps needed to ensure the desired state of software applications and libraries for business services. These playbooks are written in YAML files, which Ansible executes sequentially by connecting to target systems via SSH.
Sometimes you need certain steps in a playbook to run conditionally, such as restarting a server only when configuration files change. By default, Ansible will run these steps regardless of changes, which may not always be desirable.
Ansible addresses this with handlers, which enable conditional execution of specific tasks, ensuring that configuration changes only trigger necessary actions. They are essential for adding flexibility and responsiveness to Ansible playbooks.
In this post, we will explore handlers in Ansible, covering the following topics:
Handlers are a special type of task in Ansible that helps manage tasks that need to occur conditionally in Ansible playbooks. They don’t run unless ‘notified’ by other tasks in the sequence, for example, restarting a service after a configuration file has been modified.
Handlers differ from regular tasks in a couple of ways. First, they are not part of the sequential execution and are only executed towards the end of the playbook if notified.
Furthermore, when you trigger multiple handlers, they execute only once during the run. This ensures that these special operations are performed efficiently, without repetition, resulting in more streamlined and predictable playbook execution.
Handlers are declared using the handler
keyword in the Ansible playbook YAMLs. Other than that, they retain the same syntax for the individual tasks.
The name of each task included under the handler acts as a reference to the handlers when they are called. They are typically placed at the end of the playbook.
Handlers are triggered by tasks using the notify
or listen
attribute and are executed in the order they are defined in the handlers section. They are executed after all tasks are completed and only if a task has notified them during the play. The handler won’t run if a task that notifies a handler fails and the failure isn’t ignored.
If multiple tasks notify different handlers, Ansible collects all the notified handlers and executes them in the order they are defined in the handlers section, regardless of the order in which they were notified during the task execution. If a handler is notified multiple times during a play, it will still only run once at the end of the play.
To use a single Ansible handler, define the handler in the handlers
section and notify it from tasks using the notify
or listen
attribute.
The example below notifies the Start Apache
handler after installing the Apache server on the nodes. If the Apache server is already present on the node, the Start Apache
handler will not be called.
---
- name: Example Ansible playbook
hosts: all
become: yes
tasks:
- name: Install Apache
apt:
name: apache2
state: present
notify: Start Apache
# other regular tasks…
handlers: # handlers
- name: Start Apache
shell: /usr/sbin/apache2ctl start
args:
creates: /var/run/apache2/apache2.pid
- name: Reload Apache
shell: /usr/sbin/apache2ctl -k graceful
args:
creates: /var/run/apache2/apache2.pid
In the example above, we used the notify
attribute to notify the corresponding handler by its name. When the task is executed, it notifies the handler to execute its configuration. If not, the handler is not executed.
The same behavior can be achieved using the listen
attribute on handlers, as shown in the example below.
---
- name: Single handler demo
hosts: all
become: yes
tasks:
- name: Example task
apt:
name: package_name
state: present
notify: "example task event"
handlers:
- name: Handler 1
shell: <command>
args:
creates: /test/test1.txt
listen: "example task event"
In the playbook configuration above, when the Example task
is executed, it notifies the handlers using the generic string example task event
. Handler 1 then listens to this event and executes the code.
Using the listen
keyword decouples the dependency on the handler’s name and is a recommended approach starting with Ansible 2.2.
In certain situations, you may need to execute more than one handler. For multiple Ansible handlers, define each handler in the handlers
section, specifying unique names for each, and use notify
or listen
to trigger them from tasks. Multiple handlers can be notified from a single task, and they will run in the order they are defined if triggered by the same play.
The example below shows how to notify multiple handlers by their names from the task.
---
- name: Single handler demo
hosts: all
become: yes
tasks:
- name: Example task
apt:
name: package_name
state: present
notify:
- Handler 1
- Handler 2
handlers:
- name: Handler 1
shell: <command>
args:
creates: /test/test1.txt
- name: Handler 2
shell: <command>
args:
creates: /test/test2.txt
Here the notify
attribute specifies handler names to be called upon successful execution of the Example task.
You can achieve the same result using the listen
attribute on the handlers, as shown below.
---
- name: Single handler demo
hosts: all
become: yes
tasks:
- name: Example task
apt:
name: package_name
state: present
notify: "notify task event"
handlers:
- name: Handler 1
shell: <command>
args:
creates: /test/test1.txt
listen: "example task event"
- name: Handler 2
shell: <command>
args:
creates: /test/test2.txt
listen: "example task event"
Let’s see how handlers work in practice.
The diagram below shows an example Ansible setup. We have one control node (cnode) managing configurations on two nodes — node1 and node2. Both managed nodes are expected to run an Apache server.
Note that the above setup uses Docker containers. Depending on the deployment, you may have different SSH configurations. However, for the sake of this topic, we will not go into the details.
As the first step, include the hosts in the inventory file to get these nodes under Ansible’s management on the control node. For the above setup, the contents of the /etc/ansible/hosts on the control node are given below.
[managed_nodes]
node1 ansible_host=172.17.0.2
node2 ansible_host=172.17.0.3
[all:vars]
ansible_user=root
We have assigned the IP addresses of all the nodes under “managed_nodes” and made sure Ansible has SSH access to them. Because we want to install, configure, and run an Apache server on these nodes, we do the same using Playbook, as shown below.
---
- name: Apache installation and configuration
hosts: all
become: yes
tasks:
- name: Install Apache
apt:
name: apache2
state: present
- name: Create custom index.html
copy:
content: "Welcome to {{ ansible_hostname }}!"
dest: /var/www/html/index.html
- name: Configure Apache port
lineinfile:
path: /etc/apache2/ports.conf
regexp: '^listen '
line: 'Listen 8080'
- name: Start Apache
shell: /usr/sbin/apache2ctl start
args:
creates: /var/run/apache2/apache2.pid
Every time this playbook is executed using Ansible, the steps below are performed on each node mentioned in the control node’s inventory:
- Install Apache if it is not present
- Create an index.html file in /var/www/html/ directory by copying the provided content
- Configure the port setting to listen on 8080
- Finally, start the Apache server
The execution output below indicates that the playbook has run successfully, and the Apache servers are installed and configured on each node. Notice that each of the steps mentioned in the playbook is executed on each node.
PLAY [Apache installation and configuration] ***********************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
ok: [node2]
TASK [Install Apache] **********************************************************
ok: [node1]
ok: [node2]
TASK [Create custom index.html] ************************************************
ok: [node2]
ok: [node1]
TASK [Start Apache] ************************************************************
ok: [node2]
ok: [node1]
TASK [Configure Apache port] ***************************************************
ok: [node2]
ok: [node1]
PLAY RECAP *********************************************************************
node1 : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=5 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
You can verify the Apache service installation by curl-ing each node. The output below indicates that the Apache server is successfully serving an appropriate index.html file.
curl http://172.17.0.2
Welcome to 4e1dd32c2dce!
curl http://172.17.0.3
Welcome to 0c756fc071f7!
The issue with this approach is that when we run the Ansible Playbook again, it will always execute the start command for the Apache server, but this is not always required. To accommodate any configuration changes, we may need to restart Apache service instead of executing the start command.
At the same time, if you add a step to reload in the above Playbook, it will also always be executed. This is not desirable.
This is where handlers come into the picture. Instead of defining the start and reload tasks as generic steps in the Ansible Playbook, you can declare them as handlers instead. These handlers will not be executed by themselves. They will only be executed when another task in the playbook notifies them.
In the example above, you want to control when the start or reload commands should execute, so first, you define the start and reload steps as handlers.
You want to execute the start command only when the Apache server is first installed. Whereas, if the contents of the index.html change, or port configuration changes, you would prefer to just restart Apache server. Thus, you can specify the appropriate notify
property on each of these tasks to call the corresponding handler.
The updated Playbook is shown below.
---
- name: Apache installation and configuration
hosts: all
become: yes
tasks:
- name: Install Apache
apt:
name: apache2
state: present
notify: Start Apache
- name: Create custom index.html
copy:
content: "Welcome to {{ ansible_hostname }}!"
dest: /var/www/html/index.html
notify: Reload Apache
- name: Configure Apache port
lineinfile:
path: /etc/apache2/ports.conf
regexp: '^listen '
line: 'Listen 8080'
notify: Reload Apache
handlers:
- name: Start Apache
shell: /usr/sbin/apache2ctl start
args:
creates: /var/run/apache2/apache2.pid
- name: Reload Apache
shell: /usr/sbin/apache2ctl -k graceful
args:
creates: /var/run/apache2/apache2.pid
In the above playbook, the handlers Start Apache
and Reload Apache
are responsible for starting and reloading the server. These steps are not executed in general execution. Start Apache
is executed when the Install Apache
step notifies it, whereas Reload Apache
is executed when Create custom index.html
or Configure Apache port
steps notify it.
Execute the modified Playbook again, and observe the output.
Note that the handlers Start Apache
and Reload Apache
are not executed this time. The Apache server was already running after the previous run, so there was no need to notify the Start Apache
handler.
Because no configuration changes were made, the Reload Apache
handler did not need to be notified and executed.
PLAY [Apache installation and configuration] ***********************************************
TASK [Gathering Facts] ****************************************************
ok: [node2]
ok: [node1]
TASK [Install Apache] **************************************
ok: [node1]
ok: [node2]
TASK [Create custom index.html] ******************************************************
ok: [node2]
ok: [node1]
TASK [Configure Apache port] ********************************************
ok: [node1]
ok: [node2]
PLAY RECAP *************************************************************
node1 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
To test if handlers are indeed notified when needed, change the configuration and run the playbook again. In the updated Playbook below, we have simply changed the content of the index.html file to include a greeting.
---
- name: Apache installation and configuration
hosts: all
become: yes
tasks:
- name: Install Apache
apt:
name: apache2
state: present
notify: Start Apache
- name: Create custom index.html
copy:
content: "Good day! Welcome to {{ ansible_hostname }}!"
dest: /var/www/html/index.html
notify: Reload Apache
- name: Configure Apache port
lineinfile:
path: /etc/apache2/ports.conf
regexp: '^listen '
line: 'Listen 8080'
notify: Reload Apache
handlers:
- name: Start Apache
shell: /usr/sbin/apache2ctl start
args:
creates: /var/run/apache2/apache2.pid
- name: Reload Apache
shell: /usr/sbin/apache2ctl -k graceful
args:
creates: /var/run/apache2/apache2.pid
Rerun the playbook and observe the output below.
We can clearly see that Ansible has detected the change in the index.html file. Accordingly, it has notified the Reload Apache
handler. This confirms that you can use handlers to conditionally execute certain steps in Ansible playbooks.
PLAY [Apache installation and configuration] *********************************************
TASK [Gathering Facts] *****************************************************
ok: [node2]
ok: [node1]
TASK [Install Apache] **********************************************
ok: [node1]
ok: [node2]
TASK [Create custom index.html] ******************************************
changed: [node2]
changed: [node1]
TASK [Configure Apache port] ******************************************
ok: [node2]
ok: [node1]
RUNNING HANDLER [Reload Apache] *****************************************
ok: [node1]
ok: [node2]
PLAY RECAP *******************************************
node1 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
node2 : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Finally, let’s verify if the changes are also deployed on the nodes.
http://172.17.0.2
Good day! Welcome to 4e1dd32c2dce!
http://172.17.0.3
Good day! Welcome to 0c756fc071f7!
Handlers are a crucial concept for your playbooks. Here are some common use cases.
- Restarting services after configuration changes – This is the use case we discussed with an example above. You can use handlers to restart services after configuration changes, such as restarting an Apache or Nginx server or reloading database services after changing their configuration.
- Clearing caches – Handlers are used in flushing application caches after deploying new code or after deploying major updates on the node.
- Triggering system updates – Running system updates after installing new packages or updating firewall rules after changing network configurations are common use cases for handlers.
- Application-specific actions – Sometimes, application-specific requirements rely on the conditional execution of certain scripts. For example, you may need to rebuild search indexes when content changes or restart certain application services after code deployment, etc.
- Notification and logging – Handlers are useful when some critical changes are made to the system.
- Cleanup operations – Handlers are an excellent approach in cases where cleanup operations are required for post-decommissioning applications/services. In this case, they are used to remove temporary files or prune old backups at the appropriate time.
Spacelift’s vibrant ecosystem and excellent GitOps flow can greatly assist you in managing and orchestrating Ansible. By introducing Spacelift on top of Ansible, you can then easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization.
Another great advantage of using Spacelift is that you can manage different infrastructure tools like Ansible, Terraform, Pulumi, AWS CloudFormation, and even Kubernetes from the same place and combine their Stacks with building workflows across tools.
Our latest Ansible enhancements solve three of the biggest challenges engineers face when they are using Ansible:
- Having a centralized place in which you can run your playbooks
- Combining IaC with configuration management to create a single workflow
- Getting insights into what ran and where
Provisioning, configuring, governing, and even orchestrating your containers can be performed with a single workflow, separating the elements into smaller chunks to identify issues more easily.
Would you like to see this in action – or just want a tl;dr? Check out this video I put together showing you Spacelift’s new Ansible functionality:
If you want to learn more about using Spacelift with Ansible, check our documentation, read our Ansible guide, or book a demo with one of our engineers.
In this post, we explained the role of handlers in an Ansible playbook and their usefulness for conditionally executing some parts of the playbook. We demonstrated this using the Apache server installation and configuration change example.
Handlers are executed only once and always towards the end of any Ansible playbook, even when they are notified multiple times. This is useful to avoid unnecessary restarts, for example.
Manage Ansible Better with Spacelift
Managing large-scale playbook execution is hard. Spacelift enables you to automate Ansible playbook execution with visibility and control over resources, and seamlessly link provisioning and configuration workflows.