How to Implement Task Dependencies in Ansible with Proper Conditional Reloading


2 views

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