Abstract
| Goal | Manage task control, handlers, and task errors in Ansible Playbooks. |
| Objectives |
|
| Sections |
|
| Lab |
|
Using loops makes it possible to avoid writing multiple tasks that use the same module. For example, instead of writing five tasks to ensure that five users exist, you can write one task that iterates over a list of five users to ensure that they all exist.
To iterate a task over a set of items, you can use the loop keyword.
You can configure loops to repeat a task using each item in a list, the contents of each of the files in a list, a generated sequence of numbers, or using more complicated structures.
This section covers simple loops that iterate over a list of items. Consult the documentation for more advanced looping scenarios.
A simple loop iterates a task over a list of items.
The loop keyword is added to the task, and takes as a value the list of items over which the task should be iterated.
The loop variable item holds the value used during each iteration.
Consider the following snippet that uses the ansible.builtin.service module twice to ensure that two network services are running:
- name: Postfix is running
ansible.builtin.service:
name: postfix
state: started
- name: Dovecot is running
ansible.builtin.service:
name: dovecot
state: startedThese two tasks can be rewritten to use a simple loop so that only one task is needed to ensure that both services are running:
- name: Postfix and Dovecot are running
ansible.builtin.service:
name: "{{ item }}"
state: started
loop:
- postfix
- dovecotThe loop can use a list provided by a variable.
In the following example, the mail_services variable contains the list of services that need to be running.
vars:
mail_services:
- postfix
- dovecot
tasks:
- name: Postfix and Dovecot are running
ansible.builtin.service:
name: "{{ item }}"
state: started
loop: "{{ mail_services }}"The loop list does not need to be a list of simple values.
In the following example, each item in the list is actually a dictionary.
Each dictionary in the example has two keys, name and groups, and the value of each key in the current item loop variable can be retrieved with the item['name'] and item['groups'] variables, respectively.
- name: Users exist and are in the correct groups
user:
name: "{{ item['name'] }}"
state: present
groups: "{{ item['groups'] }}"
loop:
- name: jane
groups: wheel
- name: joe
groups: rootThe outcome of the preceding task is that the user jane is present and a member of the group wheel, and that the user joe is present and a member of the group root.
Before Ansible 2.5, most playbooks used a different syntax for loops.
Multiple loop keywords were provided, which used the with_ prefix, followed by the name of an Ansible look-up plug-in (an advanced feature not covered in detail in this course).
This syntax for looping is very common in existing playbooks, but will probably be deprecated at some point in the future.
Some examples are listed in the following table:
Table 4.1. Earlier-style Ansible Loops
| Loop keyword | Description |
|---|---|
with_items
| Behaves the same as the loop keyword for simple lists, such as a list of strings or a list of dictionaries.
Unlike loop, if lists of lists are provided to with_items, they are flattened into a single-level list.
The item loop variable holds the list item used during each iteration. |
with_file
| Requires a list of control node file names.
The item loop variable holds the content of a corresponding file from the file list during each iteration. |
with_sequence
| Requires parameters to generate a list of values based on a numeric sequence.
The item loop variable holds the value of one of the generated items in the generated sequence during each iteration. |
The following playbook shows an example of the with_items keyword:
vars:
data:
- user0
- user1
- user2
tasks:
- name: "with_items"
ansible.builtin.debug:
msg: "{{ item }}"
with_items: "{{ data }}"Since Ansible 2.5, the recommended way to write loops is to use the loop keyword.
However, you should still understand the earlier syntax, especially with_items, because it is widely used in existing playbooks.
You are likely to encounter playbooks and roles that continue to use with_* keywords for looping.
Any task using the earlier syntax can be converted to use loop in conjunction with Ansible filters.
You do not need to know how to use Ansible filters to do this.
The Ansible documentation contains a good reference on how to convert the earlier loops to the new syntax, as well as examples of how to loop over items that are not simple lists.
See the "Migrating from with_X to loop" section of the Ansible User Guide.
You might encounter tasks from earlier playbooks that contain with_* keywords.
Advanced looping techniques are beyond the scope of this course.
All iteration tasks in this course can be implemented with either the with_items or the loop keyword.
The register keyword can also capture the output of a task that loops.
The following snippet shows the structure of the register variable from a task that loops:
[student@workstation loopdemo]$cat loop_register.yml--- - name: Loop Register Test gather_facts: no hosts: localhost tasks: - name: Looping Echo Task ansible.builtin.shell: "echo This is my item: {{ item }}" loop: - one - two register: echo_results- name: Show echo_results variable ansible.builtin.debug: var: echo_results
The | |
The contents of the |
Running the preceding playbook yields the following output:
[student@workstation loopdemo]$ansible-navigator run -m stdout loop_register.ymlPLAY [Loop Register Test] ****************************************************** TASK [Looping Echo Task] ******************************************************* changed: [localhost] => (item=one) changed: [localhost] => (item=two) TASK [Show echo_results variable] ********************************************** ok: [localhost] => { "echo_results": {"changed": true, "msg": "All items completed", "results": [
{
"ansible_loop_var": "item", "changed": true, "cmd": "echo This is my item: one", "delta": "0:00:00.004519", "end": "2022-06-29 17:32:54.065165", "failed": false, ...output omitted... "item": "one", "msg": "", "rc": 0, "start": "2022-06-29 17:32:54.060646", "stderr": "", "stderr_lines": [],
"stdout": "This is my item: one","stdout_lines": [ "This is my item: one" ] }, {"ansible_loop_var": "item", "changed": true, "cmd": "echo This is my item: two", "delta": "0:00:00.004175", "end": "2022-06-29 17:32:54.296940", "failed": false, ...output omitted... "item": "two", "msg": "", "rc": 0, "start": "2022-06-29 17:32:54.292765", "stderr": "", "stderr_lines": [],
"stdout": "This is my item: two","stdout_lines": [ "This is my item: two" ] } ],"skipped": false } } ...output omitted...
The | |
The | |
The start of task metadata for the first item (indicated by the | |
The start of task result metadata for the second item. | |
The |
In the preceding example, the results key contains a list.
In the next example, the playbook is modified so that the second task iterates over this list:
[student@workstation loopdemo]$ cat new_loop_register.yml
---
- name: Loop Register Test
gather_facts: no
hosts: localhost
tasks:
- name: Looping Echo Task
ansible.builtin.shell: "echo This is my item: {{ item }}"
loop:
- one
- two
register: echo_results
- name: Show stdout from the previous task.
ansible.builtin.debug:
msg: "STDOUT from previous task: {{ item['stdout'] }}"
loop: "{{ echo_results['results'] }}"After running the preceding playbook, you see the following output:
PLAY [Loop Register Test] ******************************************************
TASK [Looping Echo Task] *******************************************************
changed: [localhost] => (item=one)
changed: [localhost] => (item=two)
TASK [Show stdout from the previous task.] *************************************
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: one', 'stderr': '', 'rc': 0, 'cmd': 'echo This is my item: one', 'start': '2022-06-29 17:41:15.558529', 'end': '2022-06-29 17:41:15.563615', 'delta': '0:00:00.005086', 'msg': '', 'invocation': {'module_args': {'_raw_params': 'echo This is my item: one', '_uses_shell': True, 'warn': False, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['This is my item: one'], 'stderr_lines': [], 'failed': False, 'item': 'one', 'ansible_loop_var': 'item'}) => {
"msg": "STDOUT from previous task: This is my item: one"
}
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: two', 'stderr': '', 'rc': 0, 'cmd': 'echo This is my item: two', 'start': '2022-06-29 17:41:15.810566', 'end': '2022-06-29 17:41:15.814932', 'delta': '0:00:00.004366', 'msg': '', 'invocation': {'module_args': {'_raw_params': 'echo This is my item: two', '_uses_shell': True, 'warn': False, 'stdin_add_newline': True, 'strip_empty_ends': True, 'argv': None, 'chdir': None, 'executable': None, 'creates': None, 'removes': None, 'stdin': None}}, 'stdout_lines': ['This is my item: two'], 'stderr_lines': [], 'failed': False, 'item': 'two', 'ansible_loop_var': 'item'}) => {
"msg": "STDOUT from previous task: This is my item: two"
}
...output omitted...Ansible can use conditionals to run tasks or plays when certain conditions are met. For example, you can use a conditional to determine available memory on a managed host before Ansible installs or configures a service.
Conditionals help you to differentiate between managed hosts and assign them functional roles based on the conditions that they meet. Playbook variables, registered variables, and Ansible facts can all be tested with conditionals. Operators to compare strings, numeric data, and Boolean values are available.
The following scenarios illustrate the use of conditionals in Ansible.
Define a hard limit in a variable (for example, min_memory) and compare it against the available memory on a managed host.
Capture the output of a command and evaluate it to determine whether a task completed before taking further action. For example, if a program fails, then a batch is skipped.
Use Ansible facts to determine the managed host network configuration and decide which template file to send (for example, network bonding or trunking).
Evaluate the number of CPUs to determine how to properly tune a web server.
Compare a registered variable with a predefined variable to determine if a service changed. For example, test the MD5 checksum of a service configuration file to see if the service is changed.
The when statement is used to run a task conditionally.
It takes as a value the condition to test.
If the condition is met, the task runs.
If the condition is not met, the task is skipped.
One of the simplest conditions that can be tested is whether a Boolean variable is true or false.
The when statement in the following example causes the task to run only if run_my_task is true.
---
- name: Simple Boolean Task Demo
hosts: all
vars:
run_my_task: true
tasks:
- name: httpd package is installed
ansible.builtin.dnf:
name: httpd
when: run_my_taskBoolean variables can have the value true or false.
In Ansible content, you can express those values in other ways: True, yes, or 1 are also accepted for true; and False, no, or 0 are also accepted for false.
You might see true and yes, or false and no used interchangeably to express Boolean values in existing Ansible content.
Ansible YAML files are based on the YAML 1.1 standard, but the YAML 1.2 standard specifies that you can only use true or false to set Boolean values.
For this reason, you might see gradual standardization toward using only true or false for Boolean values in playbooks and other Ansible files, even though the equivalent ways to express those values are still valid.
Whether Ansible should eventually use only those ways of expressing Boolean values is an open question and an ongoing discussion in the Ansible community.
When using true/false conditions such as in the preceding example, you must be very careful to make sure that your variable is treated by Ansible as a Boolean and not a string.
Starting with Ansible Core 2.12, strings are always treated by when conditionals as true Booleans if they contain any content.
(The default automation execution environment in Ansible Automation Platform 2.2 uses Ansible Core 2.13.)
Therefore, if the run_my_task variable in the preceding example were written as shown in the following example then it would be treated as a string with content and have the Boolean value true, and the task would run.
This is probably not the behavior that you want.
run_my_task: "false"
If it had been written as shown in the next example, however, it would be treated as the Boolean value false and the task would not run:
run_my_task: false
To ensure that this is the case, you could rewrite the previous when condition to convert an accidental string value to a Boolean and to pass Boolean values unchanged:
when: run_my_task | bool
The next example is a bit more sophisticated, and tests whether the my_service variable has a value.
If it does, the value of my_service is used as the name of the package to install.
If the my_service variable is not defined, then the task is skipped without an error.
---
- name: Test Variable is Defined Demo
hosts: all
vars:
my_service: httpd
tasks:
- name: "{{ my_service }} package is installed"
ansible.builtin.dnf:
name: "{{ my_service }}"
when: my_service is definedThe following table shows some operations that you can use when working with conditionals:
Table 4.2. Example Conditionals
| Operation | Example |
|---|---|
| Equal (value is a string) |
ansible_facts['machine'] == "x86_64"
|
| Equal (value is numeric) |
max_memory == 512
|
| Less than |
min_memory < 128
|
| Greater than |
min_memory > 256
|
| Less than or equal to |
min_memory <= 256
|
| Greater than or equal to |
min_memory >= 512
|
| Not equal to |
min_memory != 512
|
| Variable exists |
min_memory is defined
|
| Variable does not exist |
min_memory is not defined
|
Boolean variable is true. The values of 1, True, or yes evaluate to true. |
memory_available
|
Boolean variable is false. The values of 0, False, or no evaluate to false. |
not memory_available
|
| First variable's value is present as a value in second variable's list |
ansible_facts['distribution'] in supported_distros
|
The last entry in the preceding table might be confusing at first. The following example illustrates how it works.
In the example, the ansible_facts['distribution'] variable is a fact determined during the Gathering Facts task, and identifies the managed host's operating system distribution.
The supported_distros variable was created by the playbook author, and contains a list of operating system distributions that the playbook supports.
If the value of ansible_facts['distribution'] is in the supported_distros list, the conditional passes and the task runs.
---
- name: Demonstrate the "in" keyword
hosts: all
gather_facts: yes
vars:
supported_distros:
- RedHat
- Fedora
tasks:
- name: Install httpd using dnf, where supported
ansible.builtin.dnf:
name: http
state: present
when: ansible_facts['distribution'] in supported_distrosObserve the indentation of the when statement.
Because the when statement is not a module variable, it must be placed outside the module by being indented at the top level of the task.
A task is a YAML dictionary, and the when statement is one more key in the task, just like the task's name and the module it uses.
A common convention places any when keyword that might be present after the task's name and the module (and module arguments).
One when statement can be used to evaluate multiple conditionals.
To do so, conditionals can be combined with either the and or or keywords, and grouped with parentheses.
The following snippets show some examples of how to express multiple conditions.
If a conditional statement should be met when either condition is true, then use the or statement.
For example, the following condition is met if the machine is running either Red Hat Enterprise Linux or Fedora:
when: ansible_facts['distribution'] == "RedHat" or ansible_facts['distribution'] == "Fedora"
With the and operation, both conditions have to be true for the entire conditional statement to be met.
For example, the following condition is met if the remote host is a Red Hat Enterprise Linux 9.0 host, and the installed kernel is the specified version:
when: ansible_facts['distribution_version'] == "9.0" and ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"
The when keyword also supports using a list to describe a list of conditions.
When a list is provided to the when keyword, all the conditionals are combined using the and operation.
The example below demonstrates another way to combine multiple conditional statements using the and operator:
when: - ansible_facts['distribution_version'] == "9.0" - ansible_facts['kernel'] == "5.14.0-70.13.1.el9_0.x86_64"
This format improves readability, a key goal of well-written Ansible Playbooks.
You can express more complex conditional statements by grouping conditions with parentheses. This ensures that they are correctly interpreted.
For example, the following conditional statement is met if the machine is running either Red Hat Enterprise Linux 9 or Fedora 34. This example uses the greater-than character (>) so that the long conditional can be split over multiple lines in the playbook, to make it easier to read.
when: >
( ansible_facts['distribution'] == "RedHat" and
ansible_facts['distribution_major_version'] == "9" )
or
( ansible_facts['distribution'] == "Fedora" and
ansible_facts['distribution_major_version'] == "34" )You can combine loops and conditionals.
In the following example, the ansible.builtin.dnf module installs the mariadb-server package if there is a file system mounted on / with more than 300 MiB free.
The ansible_facts['mounts'] fact is a list of dictionaries, each one representing facts about one mounted file system.
The loop iterates over each dictionary in the list, and the conditional statement is not met unless a dictionary is found that represents a mounted file system where both conditions are true.
- name: install mariadb-server if enough space on root
ansible.builtin.dnf:
name: mariadb-server
state: latest
loop: "{{ ansible_facts['mounts'] }}"
when: item['mount'] == "/" and item['size_available'] > 300000000When you use when with loop for a task, the when statement is checked for each item.
The following example also combines conditionals and register variables.
This playbook restarts the httpd service only if the postfix service is running:
---
- name: Restart HTTPD if Postfix is Running
hosts: all
tasks:
- name: Get Postfix server status
ansible.builtin.command: /usr/bin/systemctl is-active postfix
register: result
- name: Restart Apache HTTPD based on Postfix status
ansible.builtin.service:
name: httpd
state: restarted
when: result.rc == 0
Conditionals — Ansible Documentation
What Makes A Valid Variable Name — Variables — Ansible Documentation
For more information on the change to Boolean handling in conditionals in community Ansible 5 (and Ansible Core 2.12) and later, see https://docs.ansible.com/ansible/latest/porting_guides/porting_guide_5.html#deprecated