When configuring services like Nginx with Ansible, we often face a logical dependency chain where certain tasks should only execute when specific conditions are met. The standard handler notification system sometimes forces awkward semantic relationships between tasks.
Consider this common Nginx configuration workflow:
- name: Create vhost configurations
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/{{ item }}.conf
with_items: "{{ vhosts }}"
notify: Test Nginx config
- name: Test Nginx config
command: nginx -t
listen: "Test Nginx config"
notify: Reload Nginx
- name: Reload Nginx
service:
name: nginx
state: reloaded
listen: "Reload Nginx"
The issue becomes apparent in the forced notification chain where a config test implies a reload, when semantically we want the reload to require a successful test.
We can implement proper dependencies using registered variables and conditional execution:
- name: Create vhost configurations
template:
src: templates/vhost.conf.j2
dest: /etc/nginx/sites-available/{{ item }}.conf
with_items: "{{ vhosts }}"
notify: Reload Nginx
- name: Test Nginx configuration
command: nginx -t
register: nginx_test
changed_when: false
listen: "Reload Nginx"
when: false # Never runs directly, only as handler
- name: Reload Nginx service
service:
name: nginx
state: reloaded
listen: "Reload Nginx"
when: nginx_test is defined and nginx_test.rc == 0
For more complex workflows, consider creating a custom callback plugin:
from ansible.plugins.callback import CallbackBase
class DependencyCallback(CallbackBase):
def v2_runner_on_ok(self, result):
if result.task_name == 'Test Nginx config':
if result._result.get('rc', 1) == 0:
self._display.display("Config test passed, scheduling reload")
# Logic to trigger dependent tasks
Here's a complete playbook example implementing proper dependencies:
- hosts: webservers
tasks:
- name: Ensure Nginx is installed
package:
name: nginx
state: present
- name: Deploy vhost configurations
template:
src: "{{ item }}.j2"
dest: "/etc/nginx/sites-available/{{ item }}"
loop: "{{ nginx_vhosts }}"
notify: Validate and reload Nginx
handlers:
- name: Validate Nginx config
command: nginx -t
register: nginx_validation
changed_when: false
listen: "Validate and reload Nginx"
- name: Reload Nginx if validation passed
service:
name: nginx
state: reloaded
listen: "Validate and reload Nginx"
when: nginx_validation is defined and nginx_validation.rc == 0
When working with Ansible's handler system, many developers encounter situations where the default notification behavior doesn't perfectly match their desired workflow. The classic example is managing Nginx configurations:
- name: Create vhosts
template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/{{ item }}.conf
with_items: "{{ vhosts }}"
notify: Test nginx config
- name: Test nginx config
command: nginx -t
notify: Reload nginx
- name: Reload nginx
service:
name: nginx
state: reloaded
While this works, it creates an implicit assumption that every config test should trigger a reload, which isn't logically accurate. What we really want is to say "a reload requires a successful config test" rather than "a config test implies a reload should happen."
We can implement this more accurately using a combination of handlers and explicit dependencies:
- name: Create vhosts
template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/{{ item }}.conf
with_items: "{{ vhosts }}"
notify: Reload nginx
- name: Test nginx config
command: nginx -t
register: test_result
changed_when: false
listen: "Pre-reload tasks"
- name: Reload nginx
service:
name: nginx
state: reloaded
listen: "Reload nginx"
when: test_result is defined and test_result.rc == 0
Ansible 2.2 introduced the 'listen' keyword for handlers, which allows for more flexible notification patterns. Here's how we can leverage it:
handlers:
- name: Pre-reload validation
listen: "nginx reload sequence"
command: nginx -t
register: test_result
changed_when: false
- name: Actual reload
listen: "nginx reload sequence"
service:
name: nginx
state: reloaded
when: test_result is defined and test_result.rc == 0
tasks:
- name: Create vhosts
template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/{{ item }}.conf
with_items: "{{ vhosts }}"
notify: "nginx reload sequence"
For even more control, especially when you need proper error handling:
- name: Nginx configuration management
block:
- name: Create vhosts
template:
src: vhost.conf.j2
dest: /etc/nginx/conf.d/{{ item }}.conf
with_items: "{{ vhosts }}"
- name: Test configuration
command: nginx -t
register: test_result
changed_when: false
notify: Reload nginx
rescue:
- name: Restore previous good config
command: cp /etc/nginx/conf.d/backup/* /etc/nginx/conf.d/
when: "'backup' in lookup('fileglob', '/etc/nginx/conf.d/backup/*')"
handlers:
- name: Reload nginx
service:
name: nginx
state: reloaded
when: test_result is defined and test_result.rc == 0
When implementing these patterns, remember:
- Handler notifications are still processed at the end of the play
- The order of handler execution follows their definition order
- Conditionals in handlers work the same as in regular tasks
- For complex workflows, consider breaking into separate plays