Bookmark this page

Controlling Task Execution

Objectives

  • Configure tasks that can run before roles or after normal handlers, and simultaneously notify multiple handlers.

Controlling the Order of Execution

In a play, Ansible always runs the tasks from roles, called by the roles statement, before the tasks that you define under the tasks section. The following playbook contains both a roles and a tasks section.

---
- name: Ensure Apache is deployed
  hosts: www1.example.com
  gather_facts: false

  tasks:
    - name: Open the firewall
      ansible.posix.firewalld:
        service: http
        permanent: true
        state: enabled

  roles:
    - role: apache

When you run this playbook, Ansible runs the tasks from the role before the Open the firewall task, even though the tasks section is defined first:

[user@host ~]$ ansible-navigator run deploy_apache.yml -m stdout

PLAY [Ensure Apache is deployed] *****************************************

TASK [apache : Ensure httpd packages are installed] **********************
changed: [www1.example.com]

TASK [apache : Ensure httpd service is started and enabled] **************
changed: [www1.example.com]

TASK [Open the firewall] *************************************************
changed: [www1.example.com]

PLAY RECAP ***************************************************************
www1.example.com    : ok=3    changed=3    unreachable=0  ...

For readability, it is a good practice to write the tasks section after the roles section, so that the order of the playbook matches the order of execution.

Note

This order comes from the fact that each play listed in the playbook is a YAML dictionary of key-value pairs. The order of the top-level directives (name, hosts, tasks, roles, and so on) is arbitrary, but Ansible handles them in a standardized order when it parses and runs the play.

It is a good practice to write your plays in a consistent order starting with the name of the play, but Ansible does not require this. However, deviating from this convention can make it harder to read your playbook, and therefore changing the order is not recommended.

Importing or Including Roles as a Task

In recent versions of Ansible, you can include or import roles as a task instead of by using the roles section of the play. The advantage of this approach is that you can easily run a set of tasks, import or include a role, and then run more tasks. A potential disadvantage is that it might be less clear which roles your playbook uses without closely inspecting the playbook.

Use the include_role module to dynamically include a role, and use the import_role module to statically import a role.

The following playbook demonstrates how to include a role by using a task with the include_role module.

---
- name: Executing a role as a task
  hosts: remote.example.com

  tasks:
    - name: A normal task
      ansible.builtin.debug:
        msg: 'first task'

    - name: A task to include role2 here
      ansible.builtin.include_role:
        name: role2

    - name: Another normal task
      ansible.builtin.debug:
        msg: 'second task'

With import_role, the ansible-navigator run command starts by parsing and inserting the role in the play before starting the execution. Ansible detects and reports syntax errors immediately, and does not start executing the playbook if errors are detected.

With include_role, however, Ansible parses and inserts the role in the play when it reaches the include_role task, during the play execution. If Ansible detects syntax errors in the role, then execution of the playbook is aborted.

If you use the when directive with the include_role module, then Ansible does not parse the role if the condition in the when directive is false.

Defining Pre- and Post-tasks

You might want a play that runs some tasks, and the handlers they notify, before your roles. You might also want to run tasks in the play after your normal tasks and handlers run. There are two directives you can use instead of tasks to do this:

  • pre_tasks is a tasks section that runs before the roles section.

  • post_tasks is a tasks section that runs after the tasks section and any handlers notified by tasks.

The following playbook provides an example with pre_tasks, roles, tasks, post_tasks, and handlers sections. It is unusual for a play to contain all these sections.

---
- name: Deploying New Application Version
  hosts: webservers

  pre_tasks:
    # Stop monitoring the web server to avoid sending false alarms
    # while the service is updating.
    - name: Disabling Nagios for this host
      community.general.nagios:
        action: disable_alerts
        host: "{{ inventory_hostname }}"
        services: http
      delegate_to: nagios-srv

  roles:
    - role: deploy-content

  tasks:
    - name: Restarting memcached
      ansible.builtin.service:
        name: memcached
        status: restarted
      notify: Notifying the support team

    # Confirm that the application is fully operational
    # after the update.
    - name: Validating the new deployment
      ansible.builtin.uri:
        url: "http://{{ inventory_hostname }}/healthz"
        return_content: true
      register: result
      failed_when: "'OK' not in result.content"

  post_tasks:
    - name: Enabling Nagios for this host
      community.general.nagios:
        action: enable_alerts
        host: "{{ inventory_hostname }}"
        services: http
      delegate_to: nagios-srv

  handlers:
    # Send a message to the support team through Slack
    # every time the memcached service is restarted.
    - name: Notifying the support team
      community.general.slack:
        token: G922VJP25/D923DW937/3Ffe373sfhRE6y52Fg3rvf5GlK
        msg: 'Memcached on {{ inventory_hostname }} restarted'
      delegate_to: localhost
      become: false

Reviewing the Order of Execution

Ansible runs the play sections in the following order:

  • pre_tasks

  • Handlers that are notified in the pre_tasks section

  • roles

  • tasks

  • Handlers that are notified in the roles and tasks sections

  • post_tasks

  • Handlers that are notified in the post_tasks section

The order of these sections in a play does not modify the order of execution, as given above. For example, if you write the tasks section before the roles section, then Ansible still runs the roles before the tasks in the tasks section. For readability, however, it is a good practice to organize your play in the order of execution: pre_tasks, roles, tasks, and post_tasks. You usually define the handlers at the end of the play.

Ansible runs and flushes notified handlers at several points during a run: after the pre_tasks section, after the roles and tasks sections, and after the post_tasks section. This means that a handler can run more than one time, at different times during play execution, if notified in multiple sections.

To immediately run any handlers that have been notified by a particular task in the play, add a task that uses the meta module with the flush_handlers parameter. This enables you to define specific points during task execution when all notified handlers are run.

In the following example, the play runs the Restart api server handler, if notified, after deploying a new configuration file and before using the application API. Without this call to the meta module, the play only calls the handler after running all of the tasks. If the handler has not run before the last task that uses the API, then that task might fail because the configuration file was updated, but the application has not yet reread the new configuration.

---
- name: Updating the application configuration and cache
  hosts: app_servers

  tasks:
    - name: Deploying the configuration file
      ansible.builtin.template:
        src: api-server.cfg.j2
        dest: /etc/api-server.cfg
      notify: Restart api server

    - name: Running all notified handlers
      ansible.builtin.meta: flush_handlers

    - name: Asking the API server to rebuild its internal cache
      ansible.builtin.uri:
        url: "https://{{ inventory_hostname }}/rest/api/2/cache/"
        method: POST
        force_basic_auth: true
        user: admin
        password: redhat
        body_format: json
        body:
          type: data
          delay: 0
        status_code: 201

  handlers:
    - name: Restart api server
      ansible.builtin.service:
        name: api-server
        state: restarted
        enabled: true

Remember that in a play, handlers have global scope. A play can notify handlers defined in roles. One role can notify a handler defined by another role or by the play.

Ansible always runs handlers that have been notified in the order they are listed in the handlers section of the play, and not in the order in which they were notified.

Listening to Handlers

In addition to being notified by tasks, a handler can also subscribe to a specific notification, and run when that notification is triggered. This means that one notification can trigger multiple handlers.

By default, a handler runs when a notification string matches the handler name. However, because each handler must have a unique name, the only way to trigger multiple handlers at the same time is if each one subscribes to the same notification name.

The following example shows a task that notifies My handlers when it changes, which is whenever it runs, because it has changed_when: true set. The task notifies the My handlers handler and any handler that lists My handlers in a listen directive.

---
- name: Testing the listen directive
  hosts: localhost
  gather_facts: false
  become: false

  tasks:
    - name: Trigger handlers
      ansible.builtin.debug:
        msg: Trigerring the handlers
      notify: My handlers
      changed_when: true

  handlers:
    # Listening to the "My handlers" event
    - name: Listening to a notification
      ansible.builtin.debug:
        msg: First handler was notified
      listen: My handlers

    # As an example, this handler is also triggered because
    # its name matches the notification, but no two handlers
    # can have the same name.
    - name: My handlers
      ansible.builtin.debug:
        msg: Second handler was notified

Note

In the preceding playbook, it would be better if both handlers had unique names (not My handlers) and used listen: My handlers, because the playbook would be easier to read.

The listen directive is particularly helpful when used with roles. Roles use notifications to trigger their handlers. A role can document that it notifies a certain handler when an event occurs. Other roles or a play might be able to use this notification to run additional handlers defined outside the role.

For example, a role might notify one of its handlers when a service needs restarting. In your playbook, you can define a handler that listens to that notification and performs additional tasks when Ansible restarts the service, such as sending a message to a monitoring tool, or restarting a dependent service.

As another example, you can create a role that ensures the validity of a given SSL certificate and notifies a handler that renews the certificate if it has expired. In a playbook, you can call this role to verify your Apache HTTP Server certificate validity. You can add a handler that listens for that SSL certificate renewal notification and that restarts the httpd service.

Notifying Handlers

To summarize, a task can notify multiple handlers in at least two ways:

  • It can notify a list of handlers individually by name.

  • It can notify one name for which multiple handlers are configured to listen.

If the handlers are reused as a set by multiple tasks, then the easiest way to configure your playbook is to use the second approach with the listen directive. With this approach, you need only change the handlers to mark whether they are included in the set or not, and to ensure that they are listed in the correct order.

This approach is useful when a task needs to notify a lot of handlers. Instead of notifying each handler by name, the task only sends one notification. Ansible triggers all the handlers listening to that notification. This way, you can add or remove handlers without updating the task.

In the first approach, you must find and edit every affected task to add the handler to the list. You must also ensure that the handlers are listed in the correct order in the handlers section.

Note

It is an error to have a task send a notification with no handler matching that notification.

[user@host ~]$ cat no_handler_error.yml
---
- name: Testing notification with no handler
  hosts: localhost
  gather_facts: false
  become: false

  tasks:
    - name: Task that changes
      ansible.builtin.debug:
        msg: Trigerring a non existent handler
      changed_when: true
      notify: Restart service

[user@host ~]$ ansible-navigator run -m stdout no_handler_error.yml

PLAY [Testing notification with no handler] ******************************

TASK [Task that changes] *************************************************
ERROR! The requested handler 'Restart service' was not found in either the main handlers list nor in the listening handlers list
Please review the log for errors.

Controlling the Order of Host Execution

Ansible determines which hosts to manage for a play based on the hosts directive for the play. By default, Ansible runs the play against hosts in the order in which they are listed in the inventory. You can change that order on a play-by-play basis by using the order directive.

The following playbook alphabetically sorts the hosts in the web_servers group before running the task:

---
- name: Testing host order
  hosts: web_servers
  order: sorted

  tasks:
    - name: Creating a test file in /tmp
      ansible.builtin.copy:
        content: 'This is a sample file'
        dest: /tmp/test.out

The order directive accepts the following values:

inventory

The inventory order. This is the default value.

reverse_inventory

The reverse of the inventory order.

sorted

Sorts the hosts in alphabetical order. Numbers sort before letters.

reverse_sorted

Sorts the hosts in reverse alphabetical order.

shuffle

Randomizes the host list every time you run the play.

Note

Because Ansible normally runs each task in parallel on several hosts, the output of the ansible-navigator command might not reflect the expected order; the output shows the task completion order rather than the execution order.

For example, assume that the inventory is configured so that hosts are listed in alphabetical order, and plays are run on managed hosts in inventory order (the default behavior). You could still see output like this:

...output omitted...
TASK [Creating a test file in /tmp] *********************************
changed: [www2.example.com]
changed: [www3.example.com]
changed: [www1.example.com]
...output omitted...

Notice that the task on www1.example.com completed last.

Revision: do374-2.2-82dc0d7