Table of Contents
- Can you run your ansible playbook in parallel?
- Ansible free strategy - All tasks in playbook executed without waiting for other hosts
- Ansible serial strategy - Limit playbook execution to a batch size
- More refined execution strategies - forks, throttle & run_once
- Run MULTIPLE, DIFFERENT playbooks on different hosts in PARALLEL (at the same time)
- Fastest way to execute ALL tasks across ALL hosts
- Conclusion
We look at how you can execute your Ansible playbooks in parallel as well as other playbook execution strategies.
This guide focuses more on running your playbooks in parallel, However, If you are looking to add some parallelism or asynchronous-ness within your TASKS then this is the guide for that.
Pre-requisites & Setup
- I’m using
ansible-core 2.14
- Ansible Host is running Python 3 on Ubuntu
- Managed/remote host(s) is running Python 2.7 on Ubuntu
Can you run your ansible playbook in parallel?
According to Ansible Docs, by default, each playbook task is ran on each host at the same time. The playbook does not move onto the next playbook task until each host is done with the current playbook task.
So technically each task in our playbook is running in parallel across different hosts.
Note that by default Ansible runs with 5
forks
meaning, that it will only run EACH playbook TASK on 5 hosts simultaneously (in parallel). In other words, each playbook task will be executed in batches of 5
If you don’t want a host to wait for other hosts to complete their tasks, you can use the free
strategy which just runs all tasks per host as fast as possible - more on this in the sections below.
Ansible default behavior Example - executes playbook in parallel across different hosts
Here’s an example, we have a playbook that:
- We target 2 hosts
docker-server
anddev
- Has 2 tasks, both run a python script that runs for a random amount of time (1 - 10 seconds)
- Results are shown after each task
- You will notice by looking at the timestamps that task 1 for BOTH hosts has to be completed before task 2
Note that in ansible.cfg
I use callbacks_enabled = profile_tasks
- This allows us to profile how long each task takes:
[defaults]
callbacks_enabled = profile_tasks
Playbook:
- hosts: all
gather_facts: false
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The outcome:
Task 1 Time | Task 2 Time | |
---|---|---|
host: docker-server | 0:00:10.587 (9s elapsed) | 0:00:18.779 (8s elapsed) |
host: dev | 0:00:10.587 (10s elapsed) | 0:00:18.779 (4s elapsed) |
Total task time | 10s | 8s |
Output:
Output (Click to show)
Ansible free strategy - All tasks in playbook executed without waiting for other hosts
As explained previously, using Ansible’s strategy: free
will execute your playbook’s task on each host without waiting for other hosts to complete their task.
Note: You can also set
strategy = free
in theansible.cfg
file if you want your playbooks to run using the “free” strategy by default
Free strategy Example
Here’s an example, we have a playbook that:
- We target 2 hosts
docker-server
anddev
- Has 2 tasks, both run a python script that runs for a random amount of time (1 - 10 seconds)
- Results are shown after each task
- You will notice by looking at the timestamps that task 2 for each host will execute as soon as task 1 is finished for that host
Playbook (Is the same as the last example, but with strategy: free
):
- hosts: all
gather_facts: false
strategy: free
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The outcome:
Task 1 Time | Task 2 Time | Total Host Playbook Time | |
---|---|---|---|
host: docker-server | 0:00:04.418 (4s elapsed) | 0:00:13.506 (9s elapsed) | 13s |
host: dev | 0:00:01.597 (1s elapsed) | 0:00:05.690 (4s elapsed) | 5s |
The Output:
Output (Click to show)
We can see that Task 2 for dev
host finished at 0:00:05.690 seconds. Whilst Task 2 for docker-server
finished at 0:00:13.506 seconds.
Ansible serial strategy - Limit playbook execution to a batch size
Sometimes you might want to limit the amount of hosts/machines that your playbook runs on simultaneously.
For example, you might have a playbook that downloads some file from a server, but only one machine can connect to the server at a time, so you need the playbook to be executed on one machine/host at a time - You can use the serial
strategy.
Serial Strategy Example
Here’s an example, we have a playbook that:
- We target 2 hosts
docker-server
anddev
- Has 2 tasks, both run a python script that runs for a FIXED amount of time (2 seconds)
- Results are shown after each task
serial
is set to 1, which means that our playbook is only executed on one host at a time
Playbook:
- hosts: all
gather_facts: false
serial: 1
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The Outcome:
Task 1 Time | Task 2 Time | Total Host Playbook Time | |
---|---|---|---|
host: docker-server | 0:00:02.584 (2s elapsed) | 0:00:04.696 (2s elapsed) | 4s |
host: dev | 0:00:07.228 (2s elapsed) | 0:00:09.355 (2s elapsed) | 4s |
You can see how Task 1 & 2 is first completed on the host docker-server
before being completed on the host dev
Output (Click to show)
More refined execution strategies - forks, throttle & run_once
You can use forks
, throttle
and run_once
for some finer-grain control on how you execute your tasks in your playbooks.
Forks - Limit simultaneous hosts for each task
Ansible Forks allows you to set the max number of hosts that EACH Playbook TASK can run on simultaneously.
By default, Ansible uses forks = 5
You can set Ansible forks in the ansible.cfg
:
[defaults]
forks = 10
Or, via command line when executing your playbook:
ansible-playbook -f 1 -i inventory/all.yml playbooks/parallel/tasks-normal-fixed.yml
For example:
- If your playbook targets 30 hosts and forks = 5, each playbook tasks will run on host 1, 2, 3, 4, 5 simultaneously. Then it will run the same task on host 6, 7, 8, 9, 10 simultaneously and so on…
- Once its run the task on all 30 hosts, it then moves onto the next playbook task
In the diagram below, forks = 1. Each playbook task is ran on ONE host at a time.
Example Playbook using Ansible Forks = 1
Here is the example playbook for the diagram above (forks = 1).
Playbook:
- hosts: all
gather_facts: false
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The Outcome:
Task 1 Time | Task 2 Time | |
---|---|---|
host: docker-server | 0:00:04.182 (2s elapsed) | 0:00:08.419 (2s elapsed) |
host: dev | 0:00:04.182 (2s elapsed) | 0:00:08.419 (2s elapsed) |
Total task time | 4s | 4s |
You can that because forks = 1
each task is first executed on the docker-server
host and then the dev
host - we forced Ansible to run each task sequentially across each target host.
Output (Click to show)
Throttle - Limiting a task’s concurrency using the Ansible throttle keyword
Ansible’s throttle
keyword can be used to limit the max workers for a specific playbook’s task (similar to forks
and serial
).
You can also apply the throttle keyword inside block tasks and at block-level:
- name: some task
throttle: 1 # limits to 1 worker at a time at a time
- block:
- name: some block task
throttle: 1 # limits this block's task to 1 worker at a time
throttle: 1 # limits the whole block to 1 worker at a time
Example Playbook using Ansible Throttle = 1
The Playbook - We set throttle = 1 for ONLY task 1
- hosts: all
gather_facts: false
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
throttle: 1
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_fixed.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The Outcome:
Task 1 Time | Task 2 Time | |
---|---|---|
host: docker-server | 0:00:04.806 (2s elapsed) | 0:00:06.999 (2s elapsed) |
host: dev | 0:00:04.806 (2s elapsed) | 0:00:06.999 (2s elapsed) |
Total task time | 4s | 2s |
Since Task 1 is limited to 1 worker at a time, it took 4 seconds to complete. Whereas Task 2 ran in parallel (both workers) and took 2 seconds.
Output (Click to show)
Run_once - Task only runs on one host/worker
As its name implies - the Ansible run_once
keyword means that a task only runs once, on the first host/worker. However, the result a run_once
task is still shared to all the other hosts.
- name: some task
run_once: true
If you want the task to run once but for a specific host, you can use the Ansible delegate_to
keyword. But do be wary of some tasks that cannot be delegated - more info in Ansible Docs:
- name: some task
run_once: true
delegate_to: some_remote_host
Example 1 - only task 1 is run once
- Task 1 and Task 2 execute a python script that will run for 1 - 10 seconds (random)
- Since only Task 1 is run once, Task 1 will take 4 seconds.
- When we print the results for Task 1 it will say that for both hosts, Task 1 took 4 seconds
- This is because Task 1 was only executed on host
docker-server
, but the results are shared between hostdev
and hostdocker-server
even though Task 1 did not run for hostdev
Playbook:
- hosts: all
gather_facts: false
tasks:
- name: task 1
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
run_once: true
- name: task 1 result
ansible.builtin.debug:
msg: "Task 1 Script result: {{ result.stdout_lines[0] }}"
- name: task 2
ansible.builtin.script:
cmd: ../../lib/dummy_task_random.py
executable: /usr/bin/python3
register: result
no_log: true
- name: task 2 result
ansible.builtin.debug:
msg: "Task 2 Script result: {{ result.stdout_lines[0] }}"
The Outcome:
Task 1 Time | Task 2 Time | |
---|---|---|
host: docker-server | 0:00:04.100 (4s elapsed) | 0:00:13.286 (2s elapsed) |
host: dev | N/A | 0:00:13.286 (9s elapsed) |
Total task time | 4s | 9s |
In the task output below, you’ll see that for task 1, only host docker-server
was changed
because we only ran task 1 on that host.
Output (Click to show)
Run MULTIPLE, DIFFERENT playbooks on different hosts in PARALLEL (at the same time)
So far we’ve looked at how we can run ONE playbook in parallel across different hosts.
But you might find yourself in a scenario where you have multiple different playbooks that run sequentially (e.g. by using include_playbook
in your main playbook) and you want to run them in parallel.
For example, you might have an existing scenario like this:
But you want to run your different playbooks like this:
In short, it’s like running 2 different playbooks as 2 different processes:
ansible-playbook playbook-1.yml &
ansible-playbook playbook-2.yml &
Ansible does NOT have any “quick and easy” way of doing this, and so we will use the ansible-parallel pip package which will allow us to the equivalent of running multiple ansible-playbook
commands
NOTE: You could actually refactor all your playbooks to use async on all your tasks in order to replicate this kind of behavior, but we are looking for something “quick and simple” for this specific scenario.
You CANNOT use async when importing or including a playbook, so you will end up having to merge all your playbooks and have them useasync
for parallelism whilst usingblocks
to structure them. Which can be a lot of work especially when converting a large process of sequential playbooks.
Here’s the command I used to run 2 different playbooks in parallel:
ansible-parallel tasks-normal-fixed.yml tasks-normal-random.yml
The Output:
playbooks/parallel/tasks-normal-fixed.yml: Done.
playbooks/parallel/tasks-normal-random.yml: Done.
# Playbook playbooks/parallel/tasks-normal-fixed.yml, ran in 6s
dev : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
docker-server : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
# Playbook playbooks/parallel/tasks-normal-random.yml, ran in 14s
dev : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
docker-server : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Fastest way to execute ALL tasks across ALL hosts
If you want your Ansible playbook to execute as fast as possible, then you need to use async
- tutorial here to basically “fire and forget” all your tasks.
Conclusion
There are many execution strategies when working with Ansible in order to help you achieve fast execution times whilst adding a good amount of control should you require it.
I think it’s helpful to understand all the different ways to control and execute your playbooks in order to automate your tasks in the most efficient way possible.
More info on playbook strategies in the Ansible Docs