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.
- Notification: Tasks notify handlers using the
notifykeyword - 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
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
- Use reload instead of restart: Minimizes downtime
- Batch operations: Use
listento group related handlers - Avoid unnecessary handlers: Only notify when changes occur
- Strategic flush: Use
meta: flush_handlersonly when needed - Serial execution: Control handler timing with
serialin plays - Health checks: Always verify services after restart
- Rollback capability: Implement handlers that can recover from failures
Common Mistakes to Avoid
- 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
- Learn about Conditionals and Loops for dynamic handler behavior
- Explore Roles to organize handlers effectively
- Master Troubleshooting techniques for handler issues
- Study Advanced Topics for complex orchestration
- Practice in the Playground with handler examples