Optimize your playbook to run more efficiently, and use callback plug-ins to profile and analyze which tasks consume the most time.
You can optimize your Ansible Playbooks in a number of ways. Writing efficient playbooks becomes increasingly important as the number of hosts you manage increases.
Each release of automation execution environments adds enhancements and improvements. Running the latest version of Red Hat Ansible Automation Platform might help increase the speed of your playbooks as the core components of Ansible and especially the modules provided with it are optimized over time.
An architectural optimization that you can make is to keep your control node close to the managed nodes from a networking perspective. Ansible relies heavily on network communication and the transfer of data. High latency connections or low bandwidth between the control node and its managed hosts degrade the execution time of playbooks.
Each play has a hidden task which runs first, using the ansible.builtin.setup module to collect facts from each host.
Those facts provide information about the nodes that plays can use through the ansible_facts variable.
Collecting the facts on each remote host takes time.
If you do not use those facts in your play, skip the fact gathering task by setting the gather_facts directive to false.
The following play disables fact gathering:
---
- name: Demonstrate disabling the facts gathering
hosts: web_servers
gather_facts: false
tasks:
- ansible.builtin.debug:
msg: "gather_facts is set to False"The following example uses the Linux time command to compare the execution time of the previous playbook when fact gathering is enabled, and when it is disabled.
[user@host ~]$time ansible-navigator run \>-m stdout speed_facts.yml -i inventoryPLAY [Demonstrate activating the facts gathering] ************************TASK [Gathering Facts]*************************************************** ok: [www1.example.com] ok: [www2.example.com] ok: [www3.example.com] TASK [debug] ************************************************************* ok: [www1.example.com] => { "msg": "gather_facts is set to True" } ok: [www2.example.com] => { "msg": "gather_facts is set to True" } ok: [www3.example.com] => { "msg": "gather_facts is set to True" } PLAY RECAP *************************************************************** www1.example.com : ok=1 changed=0 unreachable=0 ... www2.example.com : ok=1 changed=0 unreachable=0 ... www3.example.com : ok=1 changed=0 unreachable=0 ...real 0m6.171suser 0m2.146s sys 0m1.118s [user@host ~]$time ansible-navigator run \>-m stdout speed_nofacts.yml -i inventoryPLAY [Demonstrate disabling the facts gathering] ************************* TASK [debug] ************************************************************* ok: [www1.example.com] => { "msg": "gather_facts is set to False" } ok: [www2.example.com] => { "msg": "gather_facts is set to False" } ok: [www3.example.com] => { "msg": "gather_facts is set to False" } PLAY RECAP *************************************************************** www1.example.com : ok=1 changed=0 unreachable=0 ... www2.example.com : ok=1 changed=0 unreachable=0 ... www3.example.com : ok=1 changed=0 unreachable=0 ...real 0m1.336suser 0m1.116s sys 0m0.246s
To get the execution time of a playbook you can also use the Ansible timer callback plug-in, instead of using the time command.
Callback plug-ins are discussed later in this section.
Playbooks often use the ansible_facts['hostname'], ansible_hostname, ansible_facts['nodename'], or ansible_nodename variables to refer to the host currently being processed.
Those variables come from the fact gathering task, but you can usually replace them with the inventory_hostname and inventory_hostname_short magic variables.
Even if you disable fact gathering, you can choose to collect facts manually at any point in a play by running the ansible.builtin.setup module as a task, and the collected facts are then available for subsequent plays in the playbook.
Ansible uses cache plug-ins to store gathered facts or inventory source data gathered by a play. You can take advantage of the fact cache to limit how many times you need to gather facts by reusing facts gathered earlier.
Fact caching is always enabled.
You can only use one cache plug-in at a time.
The memory cache plug-in is enabled by default if you do not change the ansible-navigator configuration.
This plug-in caches facts gathered during the current Ansible run.
You can take advantage of this to improve performance for playbooks that contain multiple plays. The first play can gather facts for all the hosts for which you need facts in the playbook. Subsequent plays can then disable fact gathering and use the cached facts from the first play, improving performance.
The following playbook contains two simple plays and illustrates how this works.
- name: Gather facts for everyone
hosts: all
gather_facts: true
# any tasks we might want for the first play
# if you do not have tasks, "setup" will still run
- name: The next play, does not gather facts
hosts: all
gather_facts: false
tasks:
- name: Show that we still know the facts
ansible.builtin.debug:
var: ansible_factsAnother way to use fact caching is to use smart gathering.
You can set the following option for the gathering key in the ansible.cfg file to enable smart gathering:
[defaults]
gathering=smartWhen enabled, smart gathering gathers facts on each new host in a playbook run, but if the same host is used across multiple plays, then the host is not contacted for fact gathering again in the run.
If you enable smart gathering, then removing both gather_facts lines in the previous playbook sample produces the same output.
The first play gathers facts for all hosts, but the second play does not gather facts because they have already been gathered.
Fact caching also works on automation controller by default. In addition, when you edit a job template in automation controller, you can select the checkbox. This changes the fact caching plug-in to one that stores facts gathered by jobs launched by that template, so that they can be reused between multiple playbook runs. (You need to periodically run a job that gathers facts to update the automation controller fact storage if you use this feature.)
If that checkbox is not selected, automation controller uses the default memory fact caching plug-in, which only caches facts during a particular job run.
If facts might change from play to play in the same playbook, the disadvantage of relying on fact caching with the memory plug-in is that you do not get updated facts for the later plays.
You can, of course, gather facts for any play that might be affected, or run the ansible.builtin.setup module manually as a task in a play that might be affected.
You can also selectively limit fact gathering if you disable automatic fact gathering and run the ansible.builtin.setup module as an explicit task with its gather_subset option.
This is generally quicker than gathering all available facts.
The possible subsets include all, min, hardware, network, virtual, ohai, and facter.
If you exclude the all subset, then you still get the min subset.
For example, if you only want to retrieve facts in the network subset, you can include it but exclude all and min:
- name: A play that gathers some facts
hosts: all
gather_facts: false
tasks:
- name: Collect only network-related facts
ansible.builtin.setup:
gather_subset:
- '!all'
- '!min'
- networkWhen Ansible is running a play, it runs the first task on every host in the current batch, and then runs the second task on every host in the current batch, and so on until the play completes.
The forks parameter controls how many connections Ansible can have active at the same time.
By default, this is set to 5, which means that even if there are 100 hosts to process on the current task, Ansible only communicates with them in groups of five.
After it has communicated with all 100 hosts, Ansible moves to the next task.
By increasing the forks value, Ansible runs each task simultaneously on more hosts, and the playbook usually completes in less time.
For example, if you set forks to 100, Ansible can attempt to open connections to all 100 hosts in the previous example simultaneously.
This places more load on the control node, which still needs enough time to communicate with each of the hosts.
You can specify the number of forks to use in the Ansible configuration file, or you can use the -f option with the ansible-navigator command.
The following example shows the forks parameter set to 100 under the [defaults] section of the ansible.cfg configuration file.
[defaults]
forks=100Because the forks value specifies how many worker processes Ansible starts, a number that is too high might slow down your control node and your network.
Try first with a conservative value, such as 20 or 50, and increase that number step by step, each time monitoring your system resources.
Some modules accept a list of items to work on and do not require the use of a loop. This approach can increase efficiency, because the module is only called one time.
The modules for managing operating system packages work this way.
The following example uses the ansible.builtin.yum module to install several packages in a single transaction, which is the most efficient way to install a group of packages.
---
- name: Install the packages on the web servers
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure the packages are installed
ansible.builtin.yum:
name:
- httpd
- mod_ssl
- httpd-tools
- mariadb-server
- mariadb
- php
- php-mysqlnd
state: presentThe preceding playbook would be equivalent to running the following command from the shell prompt:
[root@host ~]#yum install httpd mod_ssl httpd-tools \>mariadb-server mariadb php php-mysqlnd
The following example is not efficient. It uses a loop to install the packages one at a time:
---
- name: Install the packages on the web servers
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure the packages are installed
ansible.builtin.yum:
name: "{{ item }}"
state: present
loop:
- httpd
- mod_ssl
- httpd-tools
- mariadb-server
- mariadb
- php
- php-mysqlndThe second example is equivalent to running multiple yum commands:
[root@host ~]#yum install httpd[root@host ~]#yum install mod_ssl[root@host ~]#yum install httpd-tools[root@host ~]#yum install mariadb-server[root@host ~]#yum install mariadb[root@host ~]#yum install php[root@host ~]#yum install php-mysqlnd
The second example is slower and less efficient because Ansible runs the ansible.builtin.yum module seven times, starting a process for the module seven times, and doing dependency resolution seven times.
Not all Ansible modules accept a list for the name parameter.
For example, the ansible.builtin.service module only accepts a single value for its name parameter, and you need a loop to operate on multiple items:
- name: Starting the services on the web servers
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure the services are started
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: true
loop:
- httpd
- mariadbUse the ansible-navigator doc command to get information about what types of values different module arguments can accept:
[user@host ~]$ansible-navigator doc ansible.builtin.yum -m stdout...output omitted... - name A package name or package specifier with version, like `name-1.0'. If a previous version is specified, the task also needs to turn `allow_downgrade' on. See the `allow_downgrade' documentation for caveats with downgrading packages. When using state=latest, this can be `'*'' which means run `yum -y update'. You can also pass a url or a local path to a rpm file (using state=present).To operate on several packages this can accepta comma separated string of packages or (as of 2.0)a list ofpackages.(Aliases: pkg)[Default: (null)] ...output omitted... [user@host ~]$ansible-navigator doc ansible.builtin.service -m stdout...output omitted... = name Name of the service.type: str...output omitted...
The ansible.builtin.copy module recursively copies files and directories to managed hosts.
When the directory is large, with many files, the copy can take a long time.
If you run the playbook multiple times, subsequent copies take less time because the module only copies the files that are different.
However, it is generally more efficient to use the ansible.posix.synchronize module to copy large numbers of files to managed hosts.
This module uses rsync in the background and is usually faster than the ansible.builtin.copy module.
By setting the delete option to true, the module can also remove files on the target that no longer exist on the source.
The following playbook uses the ansible.posix.synchronize module to recursively copy the web_content directory to the web servers.
---
- name: Deploy the web content on the web servers
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure web content is updated
ansible.posix.synchronize:
src: web_content/
dest: /var/www/htmlThe ansible.builtin.lineinfile module inserts or removes lines in a file, such as configuration directives in a configuration file.
The following playbook updates the Apache HTTP Server configuration file by replacing several lines.
---
- name: Configure the Apache HTTP Server
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure proper configuration of the Apache HTTP Server
ansible.builtin.lineinfile:
dest: /etc/httpd/conf/httpd.conf
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
loop:
- regexp: '^Listen 80$'
line: 'Listen 8181'
- regexp: '^ServerAdmin root@localhost'
line: 'ServerAdmin support@example.com'
- regexp: '^DocumentRoot "/var/www/html"'
line: 'DocumentRoot "/var/www/web"'
- regexp: '^<Directory "/var/www/html">'
line: '<Directory "/var/www/web">'When used with a loop, the ansible.builtin.lineinfile module is inefficient (and can be error-prone).
In this situation, use either the ansible.builtin.template or the ansible.builtin.copy module instead.
---
- name: Configure the Apache HTTP Server
hosts: web_servers
become: true
gather_facts: false
tasks:
- name: Ensure proper configuration of the Apache HTTP Server
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.confThe httpd.conf.j2 template file in the previous example is the customized version of the httpd.conf file.
To run a task on a remote node, Ansible performs several SSH operations to copy the module and all its data to the remote node and to run the module. To increase the performance of your playbook, you can activate the pipelining feature. With pipelining, Ansible establishes fewer SSH connections.
To activate pipelining, set the ANSIBLE_PIPELINING environment variable to true in the execution-environment section of the ansible-navigator.yml configuration file.
---
ansible-navigator:
ansible:
config: ./ansible.cfg
execution-environment:
image: ee-supported-rhel8:latest
pull-policy: missing
environment-variables:
set:
ANSIBLE_PIPELINING: trueAnsible does not use pipelining by default because the feature requires that the requiretty
sudo option on all the remote nodes be disabled.
On Red Hat Enterprise Linux 8, that sudo option is disabled by default, but it might be active on other systems.
To disable the option, use the visudo command to edit the /etc/sudoers file on your managed nodes and disable the requiretty option:
[root@host ~]#visudo...output omitted...Defaults !requiretty...output omitted...
Callback plug-ins extend Ansible by adjusting how it responds to various events.
Some of these plug-ins modify the output of the command-line tools, such as the ansible-navigator command, to provide additional information.
For example, the timer plug-in shows the playbook execution time in the output of the ansible-navigator command.
Automation controller logs some information about jobs (playbook runs), which it extracts from the output of ansible-navigator.
Because some callback plug-ins modify this output, you should use them with caution or avoid using them entirely, especially if you run the playbook with automation controller.
Ansible Automation Platform ships with a collection of callback plug-ins that you can enable in the ansible.cfg file by using the callbacks_enabled directive.
[defaults]
callbacks_enabled=timer, profile_tasks, cgroup_perf_recapUse the ansible-navigator doc -t callback -l -m stdout command to list the available callback plug-ins.
[user@host ~]$ ansible-navigator doc -t callback -l -m stdout
amazon.aws.aws_resource_actions summarizes all "resource:actions" completed
ansible.posix.cgroup_perf_recap Profiles system activity of tasks and full execution using cgroups
ansible.posix.debug formatted stdout/stderr display
ansible.posix.json Ansible screen output as JSON
ansible.posix.profile_roles adds timing information to roles
ansible.posix.profile_tasks adds time information to tasks
ansible.posix.skippy Ansible screen output that ignores skipped status
ansible.posix.timer Adds time to play stats
awx_display Playbook event dispatcher for ansible-runner
default default Ansible screen output
junit write playbook output to a JUnit file
minimal Ad hoc event dispatcher for ansible-runner
oneline oneline Ansible screen output
redhat.rhv.stdout Output the log of ansible
redhat.satellite.foreman Sends events to Foreman
tree Save host events to filesRun the ansible-navigator doc -t callback command to access the documentation for a specific plug-in.plug-in-name -m stdout
[user@host ~]$ansible-navigator doc -t callback cgroup_perf_recap -m stdout>ANSIBLE.POSIX.CGROUP_PERF_RECAP(/usr/share/ansible/collections/ansible_collections/ansible/posix/plugins/callback/cgroup_perf_recap.py) This is an ansible callback plugin utilizes cgroups to profile system activity of ansible and individual tasks, and display a recap at the end of the playbook execution OPTIONS (= is mandatory): = control_group Name of cgroups control group set_via: env: - name: CGROUP_CONTROL_GROUP ...output omitted...
You can use the timer, profile_tasks, and profile_roles callback plug-ins to help identify slow tasks and roles.
The timer plug-in displays the duration of playbook execution.
The profile_tasks plug-in displays the start time of each task, and the time spent on each task, sorted in descending order, at the end of the playbook execution.
The profile_roles plug-in displays the time spent on each role at the end of the output, sorted in descending order.
To activate these plug-ins, add or update the callbacks_enabled directive in the ansible.cfg file.
[defaults]
callbacks_enabled=timer, profile_tasks, profile_rolesYou do not have to enable all three plug-ins; select the ones that you need.
The following example shows the output of the ansible-navigator command when you activate the three plug-ins.
[user@host ~]$ansible-navigator run \>-m stdout deploy_webservers.yml...output omitted... PLAY RECAP *************************************************************** www1.example.com : ok=9 changed=7 unreachable=0 ... www2.example.com : ok=10 changed=9 unreachable=0 ... www3.example.com : ok=10 changed=9 unreachable=0 ... Playbook run took 0 days, 0 hours, 0 minutes, 21 seconds Wednesday 08 September 2021 19:12:31 +0000 (0:00:00.858) 0:00:21.325 *** =============================================================================== apache : Ensure httpd packages are installed ---------------------------- 7.14s haproxy : Ensure haproxy packages are present --------------------------- 1.78s apache : Ensure SELinux allows httpd connections to a remote database --- 1.72s Gathering Facts --------------------------------------------------------- 3.89s haproxy : Ensure haproxy configuration is set --------------------------- 1.12s haproxy : Ensure haproxy is started and enabled ------------------------- 0.92s webapp : Ensure stub web content is deployed ---------------------------- 0.86s firewall : Ensure Firewall Sources Configuration ------------------------ 0.85s firewall : Ensure Firewall Port Configuration --------------------------- 0.84s firewall : Ensure Firewall Service Configuration ------------------------ 0.79s apache : Ensure httpd service is started and enabled -------------------- 0.71s firewall : Firewall Port Configuration ---------------------------------- 0.70s Wednesday 08 September 2021 19:12:31 +0000 (0:00:00.867) 0:00:21.333 *** =============================================================================== apache ------------------------------------------------------------------ 9.57s gather_facts ------------------------------------------------------------ 3.89s haproxy ----------------------------------------------------------------- 3.81s firewall ---------------------------------------------------------------- 3.18s webapp ------------------------------------------------------------------ 0.86s ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ total ------------------------------------------------------------------ 21.31s
ssh_config(5) and sudoers(5) man pages