Introduction to Ansible Handlers
Sometimes you want a task to run only when a change is made on a machine. For example, you may want to restart a service if a task updates the configuration of that service, but not if the configuration is unchanged. Ansible uses handlers to address this use case. Handlers are tasks that only run when notified. Each handler should have a globally unique name.
- Handlers always run in the order they are defined, not in the order listed in the notify-statement. This is also the case for handlers using listen.
- Handler names and listen topics live in a global namespace.
- Handler names are templatable and listen topics are not.
- Use unique handler names. If you trigger more than one handler with the same name, the first one(s) get overwritten. Only the last one defined will run.
- You can notify a handler defined inside a static include.
- You cannot notify a handler defined inside a dynamic include.
Creating and Managing Handlers
In Ansible, handlers are a special kind of task used to perform actions when notified by another task. Understanding their creation and management is crucial for efficient playbook design.
Basic Syntax of Handlers
Handlers follow a syntax similar to regular tasks in Ansible. They are defined in the 'handlers' section of a playbook and are typically used for restarts or configuration changes. An Ansible Handler is triggered by a standard task using the 'notify' directive, which references the handler's name.
handlers:
- name: restart apache
service:
name: apache2
state: restarted
Handlers with Variables
Handlers can also use variables to become more dynamic. This allows a single handler to handle different scenarios based on variable values passed from tasks.
tasks:
- name: template configuration
template:
src: template.j2
dest: /etc/foo.conf
notify: "restart service {{ item }}"
with_items:
- apache
- nginx
In this example, the task notifies two handlers, one for restarting Apache and another for Nginx, based on the item variable.
Advanced Ansible Handlers Usage
In the realm of Ansible playbooks, advanced handler usage encompasses several key concepts that enhance playbook flexibility and efficiency.
1. Triggering Handlers in Playbooks
Handlers in Ansible are typically triggered by a change in a task. For instance, if a configuration file is modified, a handler might be notified to restart a service.
tasks:
- name: copy template
template:
src: template.j2
dest: /etc/foo.conf
notify: restart nginx
2. The 'Notify' Directive
The 'notify' directive in Ansible Handlers is crucial for triggering a handler. It specifies the handler name that should be alerted upon task completion.
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
3. Conditional Notifications
Ansible Handlers can also be triggered conditionally. This means the handler will only execute if certain conditions are met.
tasks:
- name: copy file
copy:
src: /src/dir/file.conf
dest: /dest/dir/file.conf
notify: restart service
when: ansible_os_family == "Debian"
4. Handlers with Loops
Using loops with handlers allows you to iterate over a list of items, potentially notifying the handler for each item.
tasks:
- name: restart multiple services
command: echo "restarting {{ item }}"
loop:
- httpd
- nginx
notify: restart service
Advanced Topics in Handlers
1. Order of Execution
In Ansible, the order of execution is vital. Handlers are typically executed at the end of a playbook or after each play. However, this can be altered using meta: flush_handlers
, which forces handlers to run immediately.
tasks:
- name: install software
apt: name=software state=latest
notify: restart software
meta: flush_handlers
2. Run Only Once and Duplicate Tasks
Handlers are designed to run only once by default, even if notified multiple times. This prevents duplicate tasks from being executed multiple times, ensuring idempotency.
handlers:
- name: restart nginx
service: name=nginx state=restarted
3. Force Handlers and Ignore Errors
You can force handlers to run even if a preceding task fails by using ignore_errors: yes
on the task. This ensures that crucial cleanup or restart handlers are executed regardless of task failures.
tasks:
- name: risky task
command: /bin/false
ignore_errors: yes
notify: critical handler
handlers:
- name: critical handler
command: /bin/echo "Handling failure"
4. Flushing Handlers
Flushing handlers with meta: flush_handlers
is a technique used to immediately execute all notified handlers. This is particularly useful in multi-stage playbooks where you need to ensure certain services are restarted or reloaded before proceeding to the next tasks.
tasks:
- name: update config
template: src=template.j2 dest=/etc/config
notify: reload service
meta: flush_handlers
tasks:
- name: continue with other tasks
Frequently Asked Questions on Ansible Handlers
Can Ansible Handlers be triggered more than once?
No, by default, Ansible Handlers are designed to be idempotent, meaning even if notified multiple times within a playbook, they will only execute once at the end of the playbook's execution.
How do I ensure a handler runs immediately after a task?
You can use the meta: flush_handlers
task to immediately run all notified handlers up to that point in the playbook.
Is it possible to use variables in Ansible Handler names?
Yes, you can use variables in handler names, but they need to be defined before the handler is called.
Can Ansible Handlers be used with loops?
Yes, handlers can be used with loops. For example, you might have a task that loops over a list of services and notifies a handler to restart each service.
How do conditionals work with Ansible Handlers?
Handlers can be conditionally triggered based on the results of a task. For instance, a handler might only execute if a task results in a change (changed_when
).
Can I use tags with Ansible Handlers?
Yes, handlers can be tagged in the same way as tasks. However, they will only execute if the task that notifies them also matches the tag criteria.
Example-1 Using Ansible Handlers
We will write a playbook ansible-handlers.yml to install httpd package and then use a handler to start the httpd service. This handler will be executed only when change is made i.e. the dependent task reports changed=1
or higher value.
---
- name: Handlers Example
hosts: server1
gather_facts: false
tasks:
- name: Install httpd latest version
yum:
name: httpd
state: latest
become: true
notify: restart_httpd
handlers:
- name: restart_httpd
become: true
service:
name: httpd
state: started
handlers
as it should start from the same line as your tasks
.In this playbook I have created a task which will install httpd rpm to the latest available version using sudo privilege. Next I have created a handler which will take care of starting the service. The handler will be executed using the value of "name" of the handler i.e. "restart_httpd" using the notify
keyword.
I have used notify
with the name of our handler
so this will check the changed mode of the task and accordingly will decide to execute the handler
.
Let us execute the playbook:
[ansible@controller ~]$ ansible-playbook ansible-handlers.yml PLAY [Handlers Example] ******************************************************************************************** TASK [Install httpd latest version] ******************************************************************************** changed: [server1] RUNNING HANDLER [restart_httpd] ************************************************************************************ changed: [server1] PLAY RECAP ********************************************************************************************************* server1 : ok=2 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
From the output we know that our TASK and HANDLER, both have executed successfully.
If we re-run the same playbook to check if handler is executed:
[ansible@controller ~]$ ansible-playbook ansible-handlers.yml PLAY [Handlers Example] ******************************************************************************************** TASK [Install httpd latest version] ******************************************************************************** ok: [server1] PLAY RECAP ********************************************************************************************************* server1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
This time there was no change performed, because httpd was already in installed state.
Example-2: Flush handlers and control when handler runs
By default, handlers run after all the tasks in a particular play have been completed. This approach is efficient, because the handler only runs once, regardless of how many tasks notify it. For example, if multiple tasks update a configuration file and notify a handler to restart Apache, Ansible only bounces Apache once to avoid unnecessary restarts.
In this sample playbook I have 2 tasks which will print some text on the console using debug module. I have also added one handler which will provide the date and time of execution. Now I am calling this handler from both the tasks:
---
- name: Handlers Example
hosts: server1
gather_facts: false
become: true
tasks:
- name: print message-1
debug:
msg: "First Message"
changed_when: true
notify: run_handler
- name: print message-2
debug:
msg: "Second Message"
changed_when: true
notify: run_handler
handlers:
- name: run_handler
debug:
msg: "Today's date and time: {{ '%d-%m-%Y %H:%M:%S' | strftime }}"
Let's verify the output if the handler is executed for both the tasks:
[ansible@controller ~]$ ansible-playbook ansible-handlers.yml PLAY [Handlers Example] ******************************************************************************************** TASK [print message-1] ********************************************************************************************* changed: [server1] => { "msg": "First Message" } TASK [print message-2] ********************************************************************************************* changed: [server1] => { "msg": "Second Message" } RUNNING HANDLER [run_handler] ************************************************************************************** ok: [server1] => { "msg": "Today's date and time: 25-09-2020 09:55:03" } PLAY RECAP ********************************************************************************************************* server1 : ok=3 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So the handler was called only once irrespective of the fact that both the tasks were calling the handler.
Now this is the default behaviour but if you need handlers to run before the end of the play, add a task
to flush them using the meta
module, which executes Ansible actions. I have updated my playbook to perform sleep
on the remote server and then observe the time with the handler to make sure we have new instance of handler being executed after every task:
---
- name: Handlers Example
hosts: server1
gather_facts: false
become: true
tasks:
- name: Sleep for 2 seconds
command: sleep 2
notify: run_handler
- name: Flush handlers
meta: flush_handlers
- name: Sleep for 5 seconds
command: sleep 5
notify: run_handler
- name: Flush handlers
meta: flush_handlers
- name: Sleep for 7 seconds
command: sleep 7
notify: run_handler
handlers:
- name: run_handler
debug:
msg: "Today's date and time: {{ '%d-%m-%Y %H:%M:%S:%s' | strftime }}"
- Here my first task will sleep for 2 minutes and then execute the handler
- Then we flush the handler and call the second task which will sleep for 5 seconds and call the handler
- Then we will again flush the handler and sleep for another 7 seconds before calling the handler for third time
- In the handler I have used
strftime
from our Jinja syntax to get the time of execution for the handler
Let us execute the playbook:
[ansible@controller ~]$ ansible-playbook ansible-handlers.yml PLAY [Handlers Example] ******************************************************************************************** TASK [Sleep for 2 seconds] ***************************************************************************************** changed: [server1] RUNNING HANDLER [run_handler] ************************************************************************************** ok: [server1] => { "msg": "Today's date and time: 25-09-2020 10:05:45:1601028345" } TASK [Sleep for 5 seconds] ***************************************************************************************** changed: [server1] RUNNING HANDLER [run_handler] ************************************************************************************** ok: [server1] => { "msg": "Today's date and time: 25-09-2020 10:05:51:1601028351" } TASK [Sleep for 7 seconds] ***************************************************************************************** changed: [server1] RUNNING HANDLER [run_handler] ************************************************************************************** ok: [server1] => { "msg": "Today's date and time: 25-09-2020 10:05:58:1601028358" } PLAY RECAP ********************************************************************************************************* server1 : ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So now our handler has executed three times because we were flushing the handler after every task execution and the time of the handler execution is also different so life is good.
Example-3: Using variables with Ansible handlers
You may want your Ansible handlers to use variables. For example, if the name of a service varies slightly by distribution, you want your output to show the exact name of the restarted service for each target machine. Avoid placing variables in the name of the handler. Since handler names are templated early on, Ansible may not have a value available for a handler name like this:
handlers:
# This handler name may cause your play to fail!
- name: Restart "{{ web_service_name }}"
If the variable used in the handler name is not available, the entire play fails. Changing that variable mid-play will not result in newly created handler.
So we should always use a default variable when in such scenarios as I have used in the sample playbook:
---
- name: Handlers Example
hosts: server1
gather_facts: false
become: true
vars:
pkg: httpd
tasks:
- name: Installing vsftpd
debug:
msg: "restarting vsftp"
changed_when: true
notify: restart vsftpd
handlers:
- name: "restart {{ pkg | default('vsftpd')}}"
debug:
msg: "Restarting {{ pkg | default('vsftpd')}}"
Now I have defined a pkg
variable but if for some reason handlers fails to get this value then it will consider the default variable value instead of failing the entire play:
Let us execute this play:
[ansible@controller ~]$ ansible-playbook ansible-handlers.yml PLAY [Handlers Example] ******************************************************************************************** TASK [Installing vsftpd] ******************************************************************************************* changed: [server1] => { "msg": "restarting vsftp" } RUNNING HANDLER [restart vsftpd] *********************************************************************************** ok: [server1] => { "msg": "Restarting vsftpd" } PLAY RECAP ********************************************************************************************************* server1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
So our playbook was successfully executed the task and the handler.
Summary
In summary, Ansible Handlers are a powerful feature in Ansible playbooks, providing a means to trigger tasks based on other task outcomes. They offer advanced functionalities such as executing only once to ensure idempotency, conditional execution, and the ability to be triggered immediately using meta: flush_handlers
. Handlers can also use loops and variables, enhancing their versatility. Understanding the order of execution, handling duplicate tasks, and managing errors are crucial for mastering Ansible Handlers. For more in-depth information and further reading, the official Ansible documentation is an invaluable resource. You can find detailed explanations and examples at the Ansible Handlers Documentation.
What's Next
Next in our Ansible Tutorial we will learn about Ansible loop which can be used to repeat certain task multiple times in the playbook.