Skip to main content

Set the order of task execution in Ansible with these two keywords

Extend Ansible's flexibility by adding pre_tasks and post_tasks to your playbooks.
Image
Woman programming in an office

Photo by Christina Morillo from Pexels

Regular readers of Enable Sysadmin know that most of us are big fans of Ansible. We particularly like using Ansible roles to design reusable code effectively. A playbook follows a specific execution order when it runs, and there are several ways to control the order in which your tasks run. In this article, I'll look at two particularly useful Ansible features, pre_tasks and post_tasks. I'll walk you through some real (and simple) examples of how these features can add additional flexibility to your playbooks by executing tasks at different points during a playbook run.

First, I'll define pre_tasks and post_tasks. You've probably already guessed what they do. Defining pre_tasks in a playbook will cause those tasks to run before all other tasks, including roles. Defining post_tasks is the opposite—these tasks will run after all others, including any handlers defined by other tasks. It's a straightforward concept, and the following examples illustrate how you can put it to work in your environment.

Managing hosts in a load balancer

My favorite aspect of Ansible is its flexibility: You can use Ansible for configuration management and as an orchestration tool to interact with multiple systems during a playbook run. I will begin with a load-balancing example. You always want to minimize disruptions to production systems, and many architectures use load balancing to accomplish this. It's usually a good idea to pull a back-end system out of a load balancer when performing disruptive work and then add it back in once the work is done. Ansible's pre_tasks and post_tasks make this easy.

The following playbook will pull a server out of an HAProxy load balancer, run full patches with a reboot, and then re-add the server after the patch role completes:

$ cat patch-webservers.yml 
---

- hosts: webservers
  pre_tasks:
    - name: Disable web server in load balancer
      community.general.haproxy:
        state: disabled
        host: '{{ inventory_hostname_short }}'
        fail_on_not_found: yes
      delegate_to: loadbalancer.example.com
  roles:
    - full_patches
  post_tasks:
    - name: Enable web server in load balancer
      community.general.haproxy:
        state: enabled
        host: '{{ inventory_hostname_short }}'
        fail_on_not_found: yes
      delegate_to: loadbalancer.example.com

Performing initial setup tasks

The pre_tasks section can also be useful for setting facts with values obtained at runtime. For example, imagine that some of your roles rely on knowing the latest available version of your software. You could get this information from your artifact server in the pre_tasks section and set a fact that can be accessed by the tasks in your roles. In this example, the latest_app_version fact is set by calling an API endpoint. Since the API call runs only if the latest_app_version isn't defined, it still allows a user to override this fact. The fact is then available to all of the roles in this playbook. It looks like this:

$ cat software-version.yml 
---
- hosts: webservers
  pre_tasks:
    - name: Get latest software version from artifact server
      ansible.builtin.uri:
        url: http://artifact-server.example.com:8080/software/latest
        return_content: yes
      delegate_to: localhost
      register: uri_output
      when: latest_app_version is not defined

    - name: Set latest_software_version fact
      ansible.builtin.set_fact:
        latest_app_version: "{{ uri_output.json.version }}"
      when: latest_app_version is not defined

    - name: Print latest_app_version
      ansible.builtin.debug:
        msg: "{{ latest_app_version }}"
  roles:
    - appserver
    - apache
    - monitored_host

If this code looks confusing, then check out my article about interacting with web endpoints using Ansible.

Muting a system in monitoring

The final example I will show is very similar to the load balancing example above. It's common to silence a host in a monitoring system prior to beginning any work on it. Once the job is complete, the host's monitoring can be reenabled. Ansible pre_tasks and post_tasks make this easy if your monitoring system supports HTTP requests, which most do:

$ cat patch-databases.yml 
---
- hosts: database_servers
  pre_tasks:
    - name: Silence host in monitoring
      ansible.builtin.uri:
        url: "http://monitoring-server.example.com:8080/hosts/{{ inventory_hostname }}/schedule_downtime"
        method: POST
        body_format: json
        body:
          downtime_duration: 30m
      delegate_to: localhost
  roles:
    - full_patches
  post_tasks:
    - name: Re-enable host in monitoring
      ansible.builtin.uri:
        url: "http://monitoring-server.example.com:8080/hosts/{{ inventory_hostname }}/clear_downtime"
        method: POST
        body_format: json
      delegate_to: localhost

A word about includes

The previous examples include tasks defined directly in the playbook. While this might be fine for one or two tasks, it can quickly make your playbooks messy. It's also not very reusable: Different playbooks would need to duplicate these tasks.

[ Need more on Ansible? Take a free technical overview course from Red Hat. Ansible Essentials: Simplicity in Automation Technical Overview. ]

You can use Ansible's built-in include capability to create cleaner, more reusable code. In the example below, I've placed my monitoring tasks into their own directory within a top-level tasks/ directory. This makes my code much cleaner and allows other playbooks to reuse these tasks:

$ cat patch-databases-with-include.yml
---
- hosts: database_servers
  pre_tasks:
    - name: Silence host in monitoring
      ansible.builtin.include: tasks/monitoring/silence-host.yml
  roles:
    - full_patches
  post_tasks:
    - name: Enable host in monitoring
      ansible.builtin.include: tasks/monitoring/enable-host.yml

Note that this tasks/ directory is in the same directory as my main playbook and not the tasks/ directory under any particular role. To make this point clearer, the directory structure looks like this:

$ tree --noreport
├── ansible.cfg
├── inventory.ini
├── patch-databases-with-include.yml
├── patch-databases.yml
├── patch-webservers.yml
├── roles
│   ├── apache
│   │   └── tasks
│   │       └── main.yml
│   ├── appserver
│   │   └── tasks
│   │       └── main.yml
│   ├── full_patches
│   │   └── tasks
│   │       └── main.yml
│   └── monitored_host
│       └── tasks
│           └── main.yml
├── software-version.yml
├── tasks
│   └── monitoring
│       ├── enable-host.yml
│       └── silence-host.yml
└── templates
    └── hosts.j2

Wrapping Up

In this article, I walked you through a few examples of how Ansible's pre_tasks and post_tasks functionality makes it easy to perform actions at the beginning and end of your playbooks. This functionality can be useful for anything from silencing hosts in monitoring to sending alerts to your internal chat tools on successful playbook runs. I'm sure you will find interesting uses for this functionality as you build out more complex playbooks in your own organization.

[ Learn more about server and configuration management by downloading the free eBook Ansible for DevOps. ]

Check out these related articles on Enable Sysadmin

Topics:   Ansible   Programming  
Author’s photo

Anthony Critelli

Anthony Critelli is a Linux systems engineer with interests in automation, containerization, tracing, and performance. He started his professional career as a network engineer and eventually made the switch to the Linux systems side of IT. He holds a B.S. and an M.S. More about me

On Demand: Red Hat Summit 2021 Virtual Experience

Relive our April event with demos, keynotes, and technical sessions from
experts, all available on demand.

Related Content

OUR BEST CONTENT, DELIVERED TO YOUR INBOX