Skip to content

Ansible Handlers: Deep Dive

  • 23 min read

Imagine you’re in the middle of a complex Ansible playbook, and everything seems to be humming along just fine. Then, boom! A critical service fails. You need a way to not just react, but to respond intelligently and with precision. This is where Ansible Handlers step in. These aren’t just afterthought tasks; they are the silent guardians of your infrastructure, ensuring that changes ripple out smoothly and only when they need to. They’re what separate the basic Ansible script from an elegant, resilient automation system. So, if you are keen to move your Ansible game to the next level, then, it’s time for a deep dive into Ansible Handlers.

What Are Ansible Handlers?

Ansible handlers are special tasks that are triggered by other tasks when they report a change. Think of them as on-call specialists, they are designed to react to specific situations, such as restarting a service after configuration changes, or reloading a firewall to apply new rules. Unlike regular tasks that execute every time, handlers are invoked only when a notification, or a “notify,” is sent to them by another task.

Handlers are declared in the handlers section of an Ansible playbook, and they can be triggered by any task that uses the notify directive. The beauty of handlers is that they ensure that an action, like a service restart, isn’t needlessly repeated. If multiple tasks report changes and notify the same handler, it executes only once, after all the tasks that generate a notification have completed.

This not only optimizes your playbooks but also makes them more efficient. Ansible handlers can be used for things such as:

  • Restarting services
  • Reloading configurations
  • Notifying other systems
  • Cleaning up temporary files

How Do Handlers Differ From Regular Tasks?

At first glance, handlers might look similar to regular tasks, as they are both defined using the same task syntax and they both perform actions. However, there are key differences that set them apart:

  • Triggered by Change: Regular tasks run every single time the play runs. Handlers, on the other hand, are event-driven, they only activate when a task signals a change by using the notify keyword.
  • Idempotency: Like regular tasks, handlers are idempotent. They avoid any unnecessary actions if the current state is aligned with what’s defined.
  • Execution Timing: Handlers run only at the end of all the normal tasks, after the entire block or the entire play (if the handler is not inside a block), this makes sure that they execute when all the changes are done. Regular tasks execute in the order that they’re listed.
  • No Direct Reference: Handlers are never called directly, instead, they are triggered by a notify directive in other tasks.

| Feature | Regular Task | Handler |
| :—————- | :——————————— | :————————————— |
| Execution | Every Playbook run | Only when notified by another task |
| Execution Timing | As defined in the tasks sequence | At the end of the notified tasks block |
| Triggered | Manually by Playbook execution | Automatically by other tasks’ changes |
| Direct Reference | Can be called directly | Not directly callable, triggered by notify |

Why Use Ansible Handlers?

The use of Ansible handlers makes sure that important actions only happen when they must. This approach prevents unnecessary service restarts and configuration reloads, leading to efficient and streamlined automation. Let’s explore the benefits in detail:

Efficient Service Management

One of the main uses for handlers is controlling services. By restarting a service only after its config has been modified, you avoid pointless downtime. This precise approach keeps your systems stable and running smoothly. For example, a web server should not restart after every minor change.

Configuration Change Precision

Handlers allow you to reload or apply configurations in response to changes. This method makes sure that config changes are activated only when needed. For instance, if your firewall config changes, it is reloaded, making the new rules effective, but only after the rules have changed. This keeps your system up to date with any new configurations, all while minimizing disruptions.

Streamlined Operations

By automating the reaction to changes, handlers help in organizing your playbooks. They let you focus on setting up your desired state. And, at the same time, make sure changes are handled correctly without you having to add more tasks. This makes your playbooks easier to read, update, and manage.

Avoiding Redundant Actions

If multiple tasks make changes that require a service restart, a handler avoids redundant actions. It collects all change notifications and acts on them only once. This method cuts back on needless work, saving time and resources.

Better Resource Use

Handlers optimize resource use by limiting actions to when they’re required. This means less downtime, quicker executions, and less strain on your system. By cutting back on unneeded service restarts and configuration reloads, handlers help save time and use your resources more effectively.

How to Define and Use Ansible Handlers

Let’s explore how to define and use Ansible handlers in detail with examples:

Defining a Handler

Handlers are defined under the handlers key in your playbook. This key is usually outside the tasks section, but it can also be within a block. Each handler is a task, with all the normal directives that a regular task uses: name, module, and arguments, etc. The key difference is that you cannot call a handler directly; instead you call a handler through another task by using the notify directive.

Here’s a simple example to demonstrate a basic handler:

handlers:
  - name: Restart Apache
    service:
      name: apache2
      state: restarted

In this handler, the name is Restart Apache, and it uses the service module to restart the apache2 service. This handler will only run if a task signals a change and notifies this handler by its name, that is, Restart Apache.

Triggering Handlers with Notify

The notify directive is how tasks signal to handlers that a change has happened. When a task uses notify, it sends a notification with the name of a handler. If the notified handler exists in the scope (block or play) that the task is in, it’s added to a queue. Once all the tasks in the scope have been processed, Ansible executes the handlers in the queue.

Let’s see how you can use this with an example:

tasks:
  - name: Modify Apache config file
    copy:
      src: apache.conf
      dest: /etc/apache2/apache2.conf
    notify: Restart Apache

In the above example, the task modifies the apache config file if the source file differs from the destination file. If this file is different from the one at the destination, the task changes the file, and, after that change, sends a notify signal to Restart Apache, the name of the handler we defined previously. If, for any reason, the source file matches the destination file, then the task does not change anything, so the notify directive is ignored, and the handler will not run.

Handler Execution Sequence

Handlers are not called immediately when they are notified. Instead, Ansible collects all the notifies, and executes the handlers at the end of all the tasks, in the order they were notified. If multiple tasks notify the same handler, the handler executes only once, after all the notifying tasks have finished. This process makes sure that changes only happen once all of the tasks in the scope have been processed.

Let’s see this in action with an example:

tasks:
  - name: Modify Apache config file
    copy:
      src: apache.conf
      dest: /etc/apache2/apache2.conf
    notify: Restart Apache
  - name: Update PHP config file
    copy:
      src: php.ini
      dest: /etc/php/7.4/apache2/php.ini
    notify: Restart Apache
  - name: Update other config
    copy:
      src: custom.conf
      dest: /etc/custom.conf
    notify: Restart other service
handlers:
  - name: Restart Apache
    service:
      name: apache2
      state: restarted
  - name: Restart other service
    service:
      name: other-service
      state: restarted

In the above example, there are two handlers: Restart Apache and Restart other service. There are also three tasks, the first two tasks notify Restart Apache and the third task notifies Restart other service. Ansible will start executing the tasks in order, and, if changes are made, it will add those handlers to a queue. After all tasks are complete, it will execute each handler only once, in the order that they were notified, that is, Restart Apache and then Restart other service. Even if there were more tasks that notified the same handler, the handler would only execute once.

Using Variables in Handlers

Handlers can also use variables, just like regular tasks. This makes it possible for you to pass data to your handler, this enables you to create dynamic handlers that adapt to various scenarios. For instance, the service name can be a variable:

tasks:
  - name: Modify Apache config file
    copy:
      src: apache.conf
      dest: /etc/apache2/apache2.conf
    notify: Restart Service
handlers:
  - name: Restart Service
    service:
      name: "{{ service_name }}"
      state: restarted

You can define the variable service_name somewhere in your playbook, for example, in the vars section.

vars:
  service_name: apache2

Or by passing a variable during playbook execution:

ansible-playbook my_playbook.yml -e "service_name=apache2"

Now, if a change is made and the handler is notified, it will restart the service defined in the service_name variable. By defining the variable during playbook execution, you can reuse this handler for multiple services.

Handlers in Blocks

Handlers can also be defined inside blocks, by defining the handlers section within the block itself. This allows you to scope handlers to a specific block of tasks. This improves the way you organize your playbook, keeping related tasks and handlers together.

Here’s an example:

tasks:
  - block:
      - name: Modify Apache config file
        copy:
          src: apache.conf
          dest: /etc/apache2/apache2.conf
        notify: Restart Apache
      - name: Update PHP config file
        copy:
          src: php.ini
          dest: /etc/php/7.4/apache2/php.ini
        notify: Restart Apache
    handlers:
      - name: Restart Apache
        service:
          name: apache2
          state: restarted
  - name: Update other config
    copy:
      src: custom.conf
      dest: /etc/custom.conf
    notify: Restart other service
handlers:
  - name: Restart other service
    service:
      name: other-service
      state: restarted

In this example, the handler Restart Apache is defined within a block. This means the Restart Apache handler will only be notified by the tasks within that block and it will execute after those tasks have finished. The Restart other service handler will be notified by the last task and will execute at the end of the playbook. By using blocks, you can group your tasks and handlers, which provides better control over execution.

Best Practices for Using Ansible Handlers

Using Ansible handlers effectively requires following some best practices. These guidelines will help you create clean, efficient, and reliable automation solutions:

Naming Convention

Use descriptive names for your handlers. A well-defined naming convention is essential for clarity and maintainability of Ansible playbooks. Choose names that reflect what the handler is doing, that way, it’s easier to understand the handler’s purpose. This makes it simple to see the connections between tasks and their handlers.

  • Good name: Restart Apache, Reload Firewall, Restart Nginx
  • Bad name: handler1, task2, do_this

Group Related Handlers

Put related handlers together in the handlers section. This makes it easier to find and manage them. If handlers are related to a specific task, then put them in the task’s block. This can improve the organization and readability of your playbooks.

Avoid Overusing Handlers

Do not overuse handlers, only notify a handler when a change is essential. If you notify a handler when no change has been made, then you are making it difficult to understand which changes are actually happening. This creates unnecessary complexity, making the playbook less efficient and harder to follow. Try to always define your tasks so that they change the state of a system only when a change is needed.

Handlers for Critical Actions

Use handlers for important actions like restarting services or reloading configurations. This allows you to control and sequence changes, minimizing risks and improving overall system stability. This approach ensures the smooth application of changes with little to no downtime.

Keep Handlers Simple

Keep your handlers as simple as possible. Complex logic or operations should be done by regular tasks. Handlers should focus on straightforward actions that respond to changes triggered by tasks. Complex operations in a handler might result in a more difficult debugging process.

Test Your Handlers

Always test your handlers to ensure they are working correctly. By testing, you can make sure that the handlers are triggered correctly and performing their intended actions. Testing also makes sure that your handlers respond properly to any change and avoid unwanted downtime.

Use Variables

Utilize variables to make your handlers more versatile. This reduces duplication and makes your playbooks more flexible. You can reuse your handler for various services, without repeating its declaration, by using variables.

Idempotency

Handlers, like regular tasks, should be idempotent, meaning that they should only apply changes when the system’s current state differs from the desired state. This ensures that handlers can be safely run without any unexpected behavior.

Documentation

Document your handlers and their usage in your playbooks. A well-written documentation makes your playbooks easier to maintain, understand and update by yourself and others. It also serves as a reference for any future tasks.

By following these best practices, you can make sure your Ansible handlers are efficient, reliable, and easy to maintain. This makes your automation process more robust, saving time and resources.

Common Mistakes to Avoid When Using Ansible Handlers

When working with Ansible handlers, it is important to avoid some common mistakes that may cause problems:

Not Understanding Trigger Logic

A common error is not fully understanding how notify and handlers interact. It’s important to know that a handler is triggered only by a notify statement and runs only once, even if called multiple times by different tasks, and at the end of the scope it’s defined in. If you don’t understand how handlers work, then, unexpected or wrong results might occur.

Overusing Handlers

Another common issue is using handlers for actions that should be regular tasks. Handlers are meant for reactive actions, such as restarting services, and not for tasks that should always run as part of your playbook. If handlers are used too much, then, your playbook might become hard to manage and debug.

Complex Logic in Handlers

Adding too much logic to handlers is a bad practice. Handlers should be simple and only perform their intended action. Complex operations can lead to debugging issues and also make your handlers difficult to understand.

Incorrect Naming

Poor naming conventions can confuse your playbooks. If your handlers do not have clear, descriptive names, it becomes harder to understand their purpose. This leads to confusion when trying to modify or maintain your playbooks.

Not Testing Handlers

Skipping the testing of your handlers can result in unexpected behavior. It’s always good practice to test if your handlers are working correctly to avoid potential issues during execution. Failing to do so can lead to errors that are difficult to fix.

Using Hardcoded Variables

Using hardcoded values in handlers can limit their flexibility. Instead, you can use variables to pass dynamic values to your handlers. Doing so, enables you to reuse the handlers for various services.

Failure to Document

Forgetting to document handlers makes your playbooks hard to understand. If you forget to document them, it can be very difficult to understand what a handler does and how it’s meant to be used, especially if you come back to them later. This also causes issues for others trying to understand your code.

Conflicting Handlers

Having handlers with similar names that perform different actions may lead to confusion and wrong results. Always make sure your handlers have unique names and clearly defined functions. Having handlers with similar names can make it difficult to maintain your playbooks, and can cause confusion in complex setups.

Ignoring Idempotency

Forgetting about idempotency can cause unintended changes, as handlers that are not idempotent will make modifications, even if they are not needed. Your handlers must only apply changes when the system state does not match the desired state. Failing to do so can cause unwanted results when running your playbooks multiple times.

Ignoring Scope

It’s important to understand the scope of a handler. If a handler is within a block, it is only notified by tasks within that same block. If a handler is defined under the handlers key outside of any block, it will only execute after the entire playbook. Neglecting the concept of scope can cause your handlers to execute when you did not intend to.

By being aware of these common mistakes and avoiding them, you can make sure that your Ansible handlers work as you expect, avoiding potential issues and making your automation processes more robust.

Advanced Handler Usage

Let’s delve into some more advanced uses of Ansible handlers:

Using Loops With Handlers

Loops in Ansible make it possible to repeat tasks, and handlers are no exception. You can use loops with handlers when the action you need to do applies to multiple items.

For example, if you have a list of services that need to be restarted, you can use a loop in your handler to iterate through all of them.

Here is how it looks:

tasks:
  - name: Modify multiple config files
    copy:
      src: "config_{{ item }}.conf"
      dest: "/etc/app/{{ item }}.conf"
    loop:
      - service1
      - service2
      - service3
    notify: Restart Service
handlers:
  - name: Restart Service
    service:
      name: "{{ item }}"
      state: restarted
    loop:
      - service1
      - service2
      - service3

The example above uses a loop to modify multiple config files. If any changes are made, it notifies the Restart Service handler. The handler also has a loop, restarting the services that had their configuration files modified. This can be very useful when you need to do the same action for multiple services or systems.

Handler Delegation

You can use the delegate_to directive in your handlers. This allows you to run a handler on a different machine from the target machine that triggered it.

Here is an example of how you can use it:

tasks:
  - name: Modify application config file
    copy:
      src: app.conf
      dest: /etc/app/app.conf
    notify: Update Load Balancer
handlers:
  - name: Update Load Balancer
    command: /path/to/load_balancer_update.sh
    delegate_to: loadbalancer.example.com

In the above example, a change on the config file notifies Update Load Balancer. Instead of executing this handler on the target machine where the changes were made, it executes on loadbalancer.example.com. This is useful for tasks that should run on specific machines.

Conditional Handlers

Just like regular tasks, you can add conditions to your handlers. This means that your handlers will only execute if certain criteria are met. This makes your playbooks more adaptable to various scenarios.

Here’s an example of how you would implement that:

tasks:
  - name: Modify app config file
    copy:
      src: app.conf
      dest: /etc/app/app.conf
    notify: Restart App
handlers:
  - name: Restart App
    service:
      name: app
      state: restarted
    when: ansible_distribution == 'Ubuntu'

In this example, the Restart App handler will only execute if the target machine is running Ubuntu. If the operating system is different, then the handler will not execute. You can use any valid condition for your handlers.

Using listen Instead of notify

Instead of using notify, you can use listen. This is not a common practice, but it makes it possible for a handler to respond to multiple events, without repeating the same notify declaration multiple times. You can create handlers that listen to different events and then execute if any of those events occur.

Here’s an example:

tasks:
  - name: Modify Apache config file
    copy:
      src: apache.conf
      dest: /etc/apache2/apache2.conf
    notify: "config change"
  - name: Update PHP config file
    copy:
      src: php.ini
      dest: /etc/php/7.4/apache2/php.ini
    notify: "config change"
handlers:
  - name: Restart Apache
    service:
      name: apache2
      state: restarted
    listen: "config change"

In the above example, two tasks notify the event config change, and the handler Restart Apache listens to this event. This also makes it possible for you to reuse one handler for multiple tasks. It works the same way as using notify, and the handler is still executed only once after all the tasks in the scope finish executing.

Trigger Handlers on Failure

Ansible makes it possible to trigger handlers when a task fails using the rescue block within a block. This is a great way to recover a system from a faulty state, and to maintain the system’s health.

Let’s see how it works:

tasks:
  - block:
      - name: Attempt operation
        command: /path/to/risky/operation.sh
        notify: Failed operation
    rescue:
      - name: Notify of failure
        command: /bin/true
        notify: Rollback operation
handlers:
  - name: Rollback operation
    command: /path/to/rollback.sh
  - name: Failed operation
    command: /bin/true

In this example, the Attempt operation command may fail. If it does, the rescue block will be executed. This block will notify Rollback operation. The Rollback operation handler will be executed at the end of the rescue block. This mechanism provides a way to make changes to the system, and then, if there are any issues, to safely revert the changes.

Using Handler Results

You can access the results of a handler, just like you can with normal tasks. This is useful for debugging and logging. You can use those results in subsequent tasks or to take different actions.

Here is an example:

tasks:
  - name: Modify configuration
    copy:
      src: app.conf
      dest: /etc/app/app.conf
    notify: Restart Application
handlers:
  - name: Restart Application
    service:
      name: app
      state: restarted
    register: restart_result
  - name: Log restart result
    debug:
      msg: "Application restart result is {{ restart_result }}"

In the example above, the Restart Application handler registers its results in the variable restart_result. Then, the next handler can access this variable, logging the result to the output. This is useful for seeing what happened when the handler was executed.

Using these advanced features, you can take your Ansible playbooks to the next level, and build a solid infrastructure automation system.

Real-World Examples of Ansible Handlers

Let’s explore a few real-world examples of how Ansible handlers are used:

Rolling Restart of a Web Cluster

A common use of handlers is to manage web server clusters. If there is a configuration change, the cluster should restart in a rolling fashion. To avoid a service outage, the cluster should restart node by node.

Here’s how you can achieve it with Ansible handlers:

tasks:
  - name: Copy new web config
    copy:
      src: web.conf
      dest: /etc/nginx/nginx.conf
    notify: Restart Web Server
  - name: Configure database connection
    template:
      src: db.conf.j2
      dest: /etc/app/db.conf
    notify: Restart Web Server
  - name: Configure other component
    template:
      src: other.conf.j2
      dest: /etc/other.conf
    notify: Restart Web Server
handlers:
  - name: Restart Web Server
    service:
      name: nginx
      state: restarted

In this example, all the config file updates notify Restart Web Server, then Ansible will restart all the nginx web servers with a rolling restart, node by node. If there are a lot of nodes, this is a safe way of restarting the cluster with no downtime.

Database Backup Triggered by Updates

Handlers are also very useful for triggering backups after updates. By doing so, you make sure that you always back up the current state of the database, in case any errors happen.

Here is an example of this use case:

tasks:
  - name: Update Database Schema
    command: /path/to/db_schema_update.sh
    notify: Database backup
  - name: Apply new application code
    copy:
      src: app.war
      dest: /opt/app/app.war
    notify: Database backup
handlers:
  - name: Database backup
    command: "/path/to/db_backup.sh"

In this example, database updates or new application code deployment notify the Database backup handler. This ensures that the database is backed up after all the relevant changes. This reduces the risk of losing data and allows you to safely deploy any new changes.

Notifying Monitoring Systems

Ansible handlers can also be used to send notifications to monitoring systems to trigger alarms or any actions after a change. This makes it possible to automatically trigger monitoring, based on any system change.

Here is how you would implement this in your playbooks:

tasks:
  - name: Update configuration
    copy:
      src: app.conf
      dest: /etc/app/app.conf
    notify: Trigger Monitoring Alert
handlers:
  - name: Trigger Monitoring Alert
    uri:
      url: "https://monitoring.example.com/trigger_alert"
      method: POST
      body: '{"message": "Configuration changed"}'
      status_code: 200
      validate_certs: false

In the example, any change in the application config will notify Trigger Monitoring Alert. The handler then calls an API on the monitoring system. This allows you to get real-time notifications when certain things have been changed.

Restart a Service Only When a Specific Configuration File Changed

You might want to restart a service only when a particular configuration file has changed, as opposed to restarting it for any configuration change. You can do this by setting up a variable within a task to signal whether the change occurred, and then use this variable in the when condition of the handler.

Here’s how it is achieved:

tasks:
  - name: Copy important config
    copy:
      src: important.conf
      dest: /etc/app/important.conf
    register: important_config
  - name: Copy normal config
    copy:
      src: normal.conf
      dest: /etc/app/normal.conf
    notify: Restart App
  - name: Check if important config was changed
    set_fact:
        restart_needed: "{{ important_config.changed }}"
handlers:
  - name: Restart App
    service:
      name: app
      state: restarted
    when: restart_needed | bool

In this example, if important.conf is changed, the task registers it, and then sets a variable named restart_needed, which is a boolean variable. The handler will then only restart the service if restart_needed is true. The normal config copy also notifies the handler, but the handler will only execute if the other copy task changed the file. This makes sure that the handler only executes when there is an important change.

By reviewing these real-world examples, you can better grasp the practical ways that Ansible handlers improve your infrastructure automation. These examples make it clear that handlers are more than simple extras; they are important components that provide the precision, control, and stability that you need when running complex automation tasks.

Ansible Handlers: An Indispensable Tool

Ansible handlers are a must-have tool when building reliable and efficient automation systems. They are key to making sure that changes to your systems happen in a smart and organized way. By triggering actions like restarting services or reloading configurations only when changes happen, handlers optimize resource use, cut down on needless operations, and streamline your automation playbooks. Whether you are managing a simple or a very complex environment, Ansible handlers make it possible for you to build a robust automation system that’s both reliable and stable. With a clear understanding of handlers and by following the best practices, you can increase your expertise in Ansible and build better solutions. So, the next time you create a playbook, remember that a well-crafted handler is the unsung hero that keeps your system running smoothly.