Set the order of task execution in Ansible with these two keywords
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. ]
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