Use Ansible Handlers Like a PRO: Don't be a Rookie


Written by - Deepak Prasad

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
NOTE:
You have to take care of the indentation for 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.

Views: 22

Deepak Prasad

He is the founder of GoLinuxCloud and brings over a decade of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels in various domains, from development to DevOps, Networking, and Security, ensuring robust and efficient solutions for diverse projects. You can reach out to him on his LinkedIn profile or join on Facebook page.

Can't find what you're searching for? Let us assist you.

Enter your query below, and we'll provide instant results tailored to your needs.

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can send mail to admin@golinuxcloud.com

Thank You for your support!!

Leave a Comment