Handlers

Introduction

Handlers are special tasks in Ansible that only run when notified by other tasks. They are primarily used to respond to changes, such as restarting services after configuration updates. Handlers are executed once at the end of a play, regardless of how many times they are notified, making them efficient for managing service restarts and similar operations.

Key Concepts:
  • Notification: Tasks notify handlers using the notify keyword
  • Deferred Execution: Handlers run at the end of the play, not immediately
  • Idempotency: A handler runs only once even if notified multiple times
  • Ordering: Handlers execute in the order they are defined, not notified

Basic Handler Usage

Simple Handler Example

The most common use case is restarting a service after configuration changes:

---
- name: Configure web server
  hosts: webservers
  become: yes

  tasks:
    - name: Copy nginx configuration
      copy:
        src: nginx.conf
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: restart nginx

    - name: Copy SSL certificate
      copy:
        src: ssl/cert.pem
        dest: /etc/nginx/ssl/cert.pem
        mode: '0600'
      notify: restart nginx

  handlers:
    - name: restart nginx
      service:
        name: nginx
        state: restarted
Pro Tip: In this example, even though two tasks notify the handler, nginx will only restart once at the end of the play.

Multiple Handlers

Define multiple handlers for different services and operations:

---
- name: Full stack deployment
  hosts: appservers
  become: yes

  tasks:
    - name: Update application code
      copy:
        src: app.jar
        dest: /opt/myapp/app.jar
      notify:
        - restart application
        - clear cache

    - name: Update nginx reverse proxy config
      template:
        src: nginx-proxy.conf.j2
        dest: /etc/nginx/conf.d/app.conf
      notify: reload nginx

    - name: Update database connection pool settings
      lineinfile:
        path: /etc/myapp/db.properties
        regexp: '^db.pool.size='
        line: 'db.pool.size=50'
      notify: restart application

  handlers:
    - name: restart application
      service:
        name: myapp
        state: restarted

    - name: reload nginx
      service:
        name: nginx
        state: reloaded

    - name: clear cache
      file:
        path: /var/cache/myapp
        state: absent

Handler Execution Order

Default Order

Handlers execute in the order they are defined in the handlers section, not in the order they are notified:

---
- name: Handler ordering demonstration
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Task 1 - notifies handler B
      debug:
        msg: "Notifying handler B"
      changed_when: true
      notify: handler B

    - name: Task 2 - notifies handler A
      debug:
        msg: "Notifying handler A"
      changed_when: true
      notify: handler A

  handlers:
    # Handlers run in definition order: A first, then B
    - name: handler A
      debug:
        msg: "Handler A executing"

    - name: handler B
      debug:
        msg: "Handler B executing"

Controlling Handler Order with Listen

Use the listen keyword to group handlers and control execution order:

---
- name: Complex handler orchestration
  hosts: webservers
  become: yes

  tasks:
    - name: Update web application
      copy:
        src: app.tar.gz
        dest: /opt/app/
      notify: deploy application

  handlers:
    - name: stop application
      service:
        name: myapp
        state: stopped
      listen: deploy application

    - name: extract application
      unarchive:
        src: /opt/app/app.tar.gz
        dest: /opt/app/
        remote_src: yes
      listen: deploy application

    - name: update permissions
      file:
        path: /opt/app
        owner: appuser
        group: appgroup
        recurse: yes
      listen: deploy application

    - name: start application
      service:
        name: myapp
        state: started
      listen: deploy application

    - name: verify application health
      uri:
        url: http://localhost:8080/health
        status_code: 200
      listen: deploy application
      retries: 5
      delay: 10

Advanced Handler Patterns

Flush Handlers

Force handlers to run immediately instead of waiting until the end of the play:

---
- name: Immediate handler execution
  hosts: databases
  become: yes

  tasks:
    - name: Update PostgreSQL configuration
      lineinfile:
        path: /etc/postgresql/14/main/postgresql.conf
        regexp: '^max_connections'
        line: 'max_connections = 200'
      notify: restart postgresql

    - name: Flush handlers now
      meta: flush_handlers

    - name: This task runs after PostgreSQL has restarted
      postgresql_db:
        name: myapp
        state: present

    - name: Run database migrations
      command: /opt/app/migrate.sh
      environment:
        DB_HOST: localhost
        DB_NAME: myapp

  handlers:
    - name: restart postgresql
      service:
        name: postgresql
        state: restarted

    - name: wait for postgresql
      wait_for:
        port: 5432
        delay: 5
        timeout: 60

Conditional Handlers

Apply conditions to handler execution:

---
- name: Conditional handler execution
  hosts: webservers
  become: yes

  tasks:
    - name: Update application configuration
      template:
        src: app-config.j2
        dest: /etc/myapp/config.yml
      notify: restart application if production

  handlers:
    - name: restart application if production
      service:
        name: myapp
        state: restarted
      when: environment == "production"

    - name: reload application if not production
      service:
        name: myapp
        state: reloaded
      when: environment != "production"

Handler with Multiple Listeners

Create handlers that respond to multiple notification topics:

---
- name: Multiple notification topics
  hosts: webservers
  become: yes

  tasks:
    - name: Update nginx config
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
      notify: web server changed

    - name: Update PHP-FPM config
      template:
        src: php-fpm.conf.j2
        dest: /etc/php/8.1/fpm/pool.d/www.conf
      notify: php changed

    - name: Update application code
      copy:
        src: app.tar.gz
        dest: /var/www/app/
      notify: application updated

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
      listen:
        - web server changed
        - application updated

    - name: restart php-fpm
      service:
        name: php8.1-fpm
        state: restarted
      listen:
        - php changed
        - application updated

    - name: clear opcache
      command: /usr/bin/php-fpm-opcache-reset
      listen:
        - php changed
        - application updated

    - name: warm up cache
      uri:
        url: http://localhost/warmup
        method: POST
      listen: application updated

Handler Chaining

Handlers Notifying Other Handlers

Handlers can notify other handlers to create execution chains:

---
- name: Handler chaining example
  hosts: appservers
  become: yes

  tasks:
    - name: Deploy new application version
      copy:
        src: app-v2.0.jar
        dest: /opt/myapp/app.jar
      notify: restart application

  handlers:
    - name: restart application
      service:
        name: myapp
        state: restarted
      notify: wait for application

    - name: wait for application
      wait_for:
        port: 8080
        delay: 5
        timeout: 60
      notify: health check

    - name: health check
      uri:
        url: http://localhost:8080/health
        status_code: 200
      retries: 10
      delay: 5
      notify: register with load balancer

    - name: register with load balancer
      uri:
        url: "http://{{ load_balancer }}/api/register"
        method: POST
        body_format: json
        body:
          host: "{{ inventory_hostname }}"
          port: 8080

    - name: send notification
      slack:
        token: "{{ slack_token }}"
        channel: "#deployments"
        msg: "Application deployed successfully on {{ inventory_hostname }}"

Handler Best Practices

Reload vs Restart

Use reload when possible to minimize downtime:

---
- name: Graceful service updates
  hosts: webservers
  become: yes

  tasks:
    - name: Update nginx virtual host configuration
      template:
        src: vhost.conf.j2
        dest: /etc/nginx/sites-available/{{ item }}
      loop:
        - example.com
        - api.example.com
      notify: reload nginx  # Reload is faster and doesn't drop connections

    - name: Install new nginx module
      apt:
        name: libnginx-mod-http-headers-more-filter
        state: present
      notify: restart nginx  # Restart required for module loading

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

    - name: restart nginx
      service:
        name: nginx
        state: restarted

Validation Before Handler Execution

Validate configuration before restarting services to prevent failures:

---
- name: Safe configuration updates
  hosts: webservers
  become: yes

  tasks:
    - name: Update nginx configuration
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        validate: 'nginx -t -c %s'
      notify: reload nginx

    - name: Update Apache configuration
      template:
        src: apache.conf.j2
        dest: /etc/apache2/apache2.conf
        validate: 'apachectl -t -f %s'
      notify: reload apache

    - name: Update Postfix configuration
      template:
        src: main.cf.j2
        dest: /etc/postfix/main.cf
      register: postfix_config
      notify: check and reload postfix

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded

    - name: reload apache
      service:
        name: apache2
        state: reloaded

    - name: check and reload postfix
      block:
        - name: Test postfix configuration
          command: postfix check
          changed_when: false

        - name: Reload postfix
          service:
            name: postfix
            state: reloaded
      rescue:
        - name: Restore backup configuration
          copy:
            src: /etc/postfix/main.cf.backup
            dest: /etc/postfix/main.cf
            remote_src: yes

        - name: Fail with message
          fail:
            msg: "Postfix configuration is invalid, restored backup"

Handlers with Error Handling

Implement robust error handling in handlers:

---
- name: Resilient handler execution
  hosts: appservers
  become: yes

  vars:
    max_restart_attempts: 3

  tasks:
    - name: Deploy application
      copy:
        src: app.jar
        dest: /opt/myapp/
      notify: restart application with retry

  handlers:
    - name: restart application with retry
      block:
        - name: Stop application
          service:
            name: myapp
            state: stopped

        - name: Clear temporary files
          file:
            path: /tmp/myapp
            state: absent

        - name: Start application
          service:
            name: myapp
            state: started
          register: restart_result
          retries: "{{ max_restart_attempts }}"
          delay: 10
          until: restart_result is succeeded

        - name: Verify application health
          uri:
            url: http://localhost:8080/health
            status_code: 200
          retries: 20
          delay: 5

      rescue:
        - name: Restore previous version
          copy:
            src: /opt/myapp/app.jar.backup
            dest: /opt/myapp/app.jar
            remote_src: yes

        - name: Start with backup version
          service:
            name: myapp
            state: started

        - name: Send failure notification
          mail:
            to: ops-team@example.com
            subject: "Application restart failed on {{ inventory_hostname }}"
            body: "Failed to restart application after {{ max_restart_attempts }} attempts"

        - name: Fail the play
          fail:
            msg: "Application restart failed, rolled back to previous version"

      always:
        - name: Log restart attempt
          lineinfile:
            path: /var/log/myapp/restart.log
            line: "{{ ansible_date_time.iso8601 }} - Restart attempted by Ansible"
            create: yes

Force Handlers

Running Handlers on Failure

Use force_handlers to run handlers even when tasks fail:

---
- name: Force handlers on failure
  hosts: webservers
  become: yes
  force_handlers: yes

  tasks:
    - name: Update application configuration
      template:
        src: config.j2
        dest: /etc/myapp/config.yml
      notify: restart application

    - name: This task might fail
      command: /usr/local/bin/risky-operation.sh
      # Even if this fails, handlers will still run due to force_handlers

    - name: Deploy new application
      copy:
        src: app.jar
        dest: /opt/myapp/
      notify: restart application

  handlers:
    - name: restart application
      service:
        name: myapp
        state: restarted

Handlers in Roles

Role Handler Organization

Handlers in roles are defined in roles/ROLE_NAME/handlers/main.yml:

# roles/nginx/handlers/main.yml
---
- name: restart nginx
  service:
    name: nginx
    state: restarted

- name: reload nginx
  service:
    name: nginx
    state: reloaded

- name: validate nginx config
  command: nginx -t
  changed_when: false

- name: test nginx configuration and reload
  block:
    - name: test configuration
      command: nginx -t
      changed_when: false

    - name: reload if valid
      service:
        name: nginx
        state: reloaded
  rescue:
    - name: configuration invalid
      fail:
        msg: "Nginx configuration test failed"

Using role handlers in a playbook:

---
- name: Deploy with role handlers
  hosts: webservers
  become: yes

  roles:
    - nginx

  tasks:
    - name: Deploy custom nginx configuration
      template:
        src: custom-site.conf.j2
        dest: /etc/nginx/sites-available/custom-site.conf
      notify: reload nginx  # Notifies handler from nginx role

Real-World Handler Patterns

Database Migration with Handlers

---
- name: Application deployment with database migration
  hosts: appservers
  become: yes
  serial: 1

  tasks:
    - name: Stop application
      service:
        name: myapp
        state: stopped

    - name: Deploy new application version
      copy:
        src: app-v{{ app_version }}.jar
        dest: /opt/myapp/app.jar
      notify:
        - run database migrations
        - start application
        - health check

    - name: Update database configuration
      template:
        src: database.properties.j2
        dest: /etc/myapp/database.properties
      notify: start application

  handlers:
    - name: run database migrations
      command: /opt/myapp/bin/migrate.sh
      run_once: true
      delegate_to: "{{ groups['appservers'][0] }}"

    - name: start application
      service:
        name: myapp
        state: started

    - name: health check
      uri:
        url: http://localhost:8080/health
        status_code: 200
      retries: 30
      delay: 10
      register: health_result

    - name: rollback on failure
      block:
        - name: Stop failed application
          service:
            name: myapp
            state: stopped
          when: health_result is failed

        - name: Restore previous version
          copy:
            src: /opt/myapp/app.jar.backup
            dest: /opt/myapp/app.jar
            remote_src: yes
          when: health_result is failed

        - name: Rollback database
          command: /opt/myapp/bin/rollback.sh
          run_once: true
          delegate_to: "{{ groups['appservers'][0] }}"
          when: health_result is failed

Certificate Renewal Handler Chain

---
- name: SSL certificate management
  hosts: webservers
  become: yes

  tasks:
    - name: Renew Let's Encrypt certificate
      command: certbot renew --quiet
      register: cert_renewal
      changed_when: "'renewed' in cert_renewal.stdout"
      notify: certificate renewed

  handlers:
    - name: certificate renewed
      debug:
        msg: "Certificate was renewed, triggering service updates"
      notify:
        - reload nginx
        - reload apache
        - restart haproxy

    - name: reload nginx
      service:
        name: nginx
        state: reloaded
      when: "'nginx' in ansible_facts.services"

    - name: reload apache
      service:
        name: apache2
        state: reloaded
      when: "'apache2' in ansible_facts.services"

    - name: restart haproxy
      service:
        name: haproxy
        state: restarted
      when: "'haproxy' in ansible_facts.services"

    - name: verify ssl configuration
      uri:
        url: "https://{{ ansible_fqdn }}"
        validate_certs: yes
      delegate_to: localhost

    - name: update monitoring
      uri:
        url: "http://monitoring.example.com/api/certs/{{ inventory_hostname }}"
        method: PUT
        body_format: json
        body:
          hostname: "{{ inventory_hostname }}"
          renewed_at: "{{ ansible_date_time.iso8601 }}"

Blue-Green Deployment with Handlers

---
- name: Blue-Green deployment
  hosts: localhost
  gather_facts: no

  vars:
    active_pool: blue
    inactive_pool: green

  tasks:
    - name: Deploy to inactive pool
      command: /usr/local/bin/deploy.sh {{ inactive_pool }}
      notify:
        - verify inactive pool
        - switch load balancer
        - drain active pool

  handlers:
    - name: verify inactive pool
      uri:
        url: "http://{{ item }}/health"
        status_code: 200
      loop: "{{ groups[inactive_pool] }}"
      retries: 20
      delay: 5

    - name: switch load balancer
      command: /usr/local/bin/switch-pool.sh {{ inactive_pool }}
      delegate_to: "{{ groups['loadbalancer'][0] }}"
      notify: monitor new pool

    - name: monitor new pool
      pause:
        minutes: 5
        prompt: "Monitoring {{ inactive_pool }} pool. Press Ctrl+C to rollback"
      notify: finalize deployment

    - name: drain active pool
      command: /usr/local/bin/drain-pool.sh {{ active_pool }}
      delegate_to: "{{ groups['loadbalancer'][0] }}"

    - name: finalize deployment
      debug:
        msg: "Deployment successful. {{ inactive_pool }} is now active"

Handler Debugging

Troubleshooting Handler Execution

---
- name: Debug handler execution
  hosts: localhost
  gather_facts: no

  tasks:
    - name: Trigger handler with debug
      debug:
        msg: "This task will notify the handler"
      changed_when: true
      notify: debug handler

    - name: Check if handler will run
      debug:
        msg: "Handlers notified: {{ ansible_play_handlers }}"

  handlers:
    - name: debug handler
      debug:
        msg: "Handler executed at {{ ansible_date_time.iso8601 }}"
        verbosity: 0

    - name: detailed handler debugging
      debug:
        msg: |
          Handler variables:
          - Inventory hostname: {{ inventory_hostname }}
          - Play hosts: {{ ansible_play_hosts }}
          - Handler name: {{ ansible_handler_name | default('N/A') }}

Performance Considerations

Handler Performance Tips:
  • Use reload instead of restart: Minimizes downtime
  • Batch operations: Use listen to group related handlers
  • Avoid unnecessary handlers: Only notify when changes occur
  • Strategic flush: Use meta: flush_handlers only when needed
  • Serial execution: Control handler timing with serial in plays
  • Health checks: Always verify services after restart
  • Rollback capability: Implement handlers that can recover from failures

Common Mistakes to Avoid

Common Handler Pitfalls:
  • Handler name typos: Ensure notify and handler names match exactly
  • Expecting immediate execution: Remember handlers run at play end
  • Forgetting changed_when: Handlers only run when tasks report changes
  • Missing flush_handlers: Flush when subsequent tasks depend on handler completion
  • No error handling: Handlers can fail; implement rescue blocks
  • Circular notifications: Avoid handlers that notify each other infinitely
  • Duplicate handler names: Each handler must have a unique name

Next Steps