6 - Run tasks multiple times
Objective
Get to know the loop
, with_<lookup>
, and until
keywords to execute a task multiple times.
The loop
keyword is not yet a full replacement for with_<lookup>
, but we recommend it for most use cases. Both keywords achieve the same thing (although a bit differently under the hood).
You may find e.g. with_items
in examples and the use is not (yet) deprecated - that syntax will still be valid for the foreseeable future - but try to use the loop
keyword whenever possible.
The until
keyword is used to retry a task until a certain condition is met. For example, you could run a task up to X
times (defined by a retries parameter) with a delay of X
seconds between each attempt. This may be useful if your playbook has to wait for the startup of a process before continuing.
Guide
Loops enable us to repeat the same task over and over again. For example, lets say you want to create multiple users. By using an Ansible loop, you can do that in a single task. Loops can also iterate over more than just basic lists. For example, if you have a list of users with their corresponding group, loop can iterate over them as well.
Find out more about loops in the Ansible Loops documentation.
Step 1 - Simple Loops
To show the loops feature we will generate three new users on node1
. For that, create the file loop_users.yml
in ~/ansible-files
on your control node as your student user. We will use the user
module to generate the user accounts.
---
- name: Demo playbook for loops
hosts: node1
become: true
tasks:
- name: Ensure multiple users are present
ansible.builtin.user:
name: "{{ item }}"
state: present
loop:
- dev_user
- qa_user
- prod_user
Understand the playbook and the output:
- The names are not provided to the user module directly. Instead, there is only a variable called
{{ item }}
for the parametername
. - The
loop
keyword lists the actual user names. Those replace the{{ item }}
during the actual execution of the playbook. - During execution the task is only listed once, but there are three changes listed underneath it.
Step 2 - Loops over hashes
As mentioned loops can also be over lists of hashes (multiple key-value-pairs in every list item). Imagine that the users should be assigned to different additional groups:
- username: dev_user
group: ftp
- username: qa_user
group: apache
- username: prod_user
group: admin
The user
module has the optional parameter groups
which defines the group (or list of groups) the user should be added to. To reference items in a hash, the {{ item }}
keyword needs to reference the sub-key: {{ item.group }}
for example.
Hint
By default, the user is removed from all other groups. Use the module parameter append: true
to modify this.
Let's rewrite the playbook to create the users with additional user rights:
---
- name: Demo playbook for loops
hosts: node1
become: true
tasks:
- name: Ensure multiple users are present
ansible.builtin.user:
name: "{{ item.username }}"
state: present
groups: "{{ item.group }}"
loop:
- username: dev_user
group: ftp
- username: qa_user
group: apache
- username: prod_user
group: admin
Check the output:
- Again the task is listed once, but three changes are listed. Each loop item with its content is shown.
Failure
At least one user was not created because of a missing group, the playbook failed?
Well, we did not create all groups, the user-module does not do this! Some groups are already present, either they were present by default or were created when we installed packages, other groups must be created before we can use them.
Success
To ensure all groups are created, before you reference them, add one more task which creates the groups for you!
Use the ansible.builtin.group
module and loop over the same list as the task which creates the users, this list contains all groups which need to be created.
Need help?
---
- name: Demo playbook for loops
hosts: node1
become: true
tasks:
# Looping over same list as the next task, but only using/referencing the groups key
- name: Ensure groups are present
ansible.builtin.group:
name: "{{ item.group }}"
state: present
loop:
- username: dev_user
group: ftp
- username: qa_user
group: apache
- username: prod_user
group: admin
- name: Ensure multiple users are present
ansible.builtin.user:
name: "{{ item.username }}"
state: present
groups: "{{ item.group }}"
loop:
- username: dev_user
group: ftp
- username: qa_user
group: apache
- username: prod_user
group: admin
Instead of repeating the list in the loop, you can (and should!) relocate the loop content to a variable and reference this one. Take a look at the following playbook:
---
- name: Demo playbook for loops
hosts: node1
become: true
vars:
user_and_group_list:
- username: dev_user
group: ftp
- username: qa_user
group: apache
- username: prod_user
group: admin
tasks:
- name: Ensure groups are present
ansible.builtin.group:
name: "{{ item.group }}"
state: present
loop: "{{ user_and_group_list }}"
- name: Ensure multiple users are present
ansible.builtin.user:
name: "{{ item.username }}"
state: present
groups: "{{ item.group }}"
loop: "{{ user_and_group_list }}"
Run the playbook again to ensure all users (and groups) are created!
Afterwards, verify that the user prod_user
was indeed created on node1
using the following playbook, name it user_id.yml
:
---
- name: Get user ID play
hosts: node1
vars:
myuser: "prod_user"
tasks:
- name: Get info for {{ myuser }}
ansible.builtin.getent:
database: passwd
key: "{{ myuser }}"
- name: Output info for {{ myuser }}
ansible.builtin.debug:
msg: "{{ myuser }} uid: {{ getent_passwd[myuser][1] }}"
$ ansible-playbook user_id.yml
PLAY [Get user ID play] ******************************************************************************************
TASK [Gathering Facts] *******************************************************************************************
ok: [node1]
TASK [Get info for prod_user] *****************************************************************************************
ok: [node1]
TASK [Output info for prod_user] **************************************************************************************
ok: [node1] => {
"msg": [
"prod_user uid: 1002"
]
}
PLAY RECAP *******************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ ansible-navigator run user_id.yml -m stdout
PLAY [Get user ID play] ******************************************************************************************
TASK [Gathering Facts] *******************************************************************************************
ok: [node1]
TASK [Get info for prod_user] *****************************************************************************************
ok: [node1]
TASK [Output info for prod_user] **************************************************************************************
ok: [node1] => {
"msg": [
"prod_user uid: 1002"
]
}
PLAY RECAP *******************************************************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Hint
It is possible to insert a string directly into the dictionary structure like this (although it makes the task less flexible):
- name: Output info for user
ansible.builtin.debug:
msg: "{{ myuser }} uid: {{ getent_passwd[myuser]['prod_user'][1] }}"
prod_user
) of the variable myuser
is used directly. It must be enclosed in single quotes. You can't use normal quotation marks, as these are used outside of the whole variable.
Step 3 - Loops with list-variable
Up to now, we always provided the list to loop in the loop keyword directly, most of the times you will provide the list with a variable.
---
- name: Use Ansible magic variables
hosts: control
tasks:
- name: Show all the hosts in the inventory
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ groups['all'] }}"
- name: Show all the hosts in the current play
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ ansible_play_hosts }}"
This playbook uses two magic variables, these variables cannot be set directly by the user and are always defined.
The second task for example, uses the special variable ansible_play_hosts
, which contains a list of hosts in the current play run, failed or unreachable hosts are excluded from this list. The first task uses the special variable groups
, this contains a dictionary with all the groups in the inventory and each group has the list of hosts that belong to it.
Copy the contents to a file special-variables.yml
and run the playbook.
We can use the playbook to display that the loop keyword needs list-input, if you provide otherwise, Ansible will display an error message.
fatal: [node1]: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list, got this instead: {'all': ['node1', 'node2', 'node3'], 'ungrouped': [], 'web': ['node1', 'node2', 'node3']}. Hint: If you passed a list/dict of just one element, try adding wantlist=True to your lookup invocation or use q/query instead of lookup."}
You can provoke this, if you change line 8 to loop: "{{ groups }}"
. With that change you would try to loop a dictionary, this obviously fails.