Chapter #5: Ansible Loops
You may sometimes want to repeat a task multiple times. For example, you may want to create multiple users, start/stop multiple services, or change ownership on several files on your managed hosts.
In this tutorial, you will learn how to use Ansible loops to repeat a task multiple times without having to rewrite the whole task over and over again.
Before you look at loops in Ansible, I hope you have followed other chapters in this Ansible tutorial series. You should know the concept of Ansible playbooks, aware of the ad-hoc commands and know the basic terminology associated with Ansible like list, dictionaries etc.
Knowing the basics of YAML is also appreciated.
Looping over lists
Ansible uses the keywords loop to iterate over the elements of a list. To demonstrate, let’s create a very simple playbook named print-list.yml that shows you how to print the elements in a list:
[elliot@control plays]$ cat print-list.yml
---
- name: print list
hosts: node1
vars:
prime: [2,3,5,7,11]
tasks:
- name: Show first five prime numbers
debug:
msg: "{{ item }}"
loop: "{{ prime }}"
Notice that I use the item variable with Ansible loops. The task would run five times which is equal to the number of elements in the prime list.
On the first run, the item variable will be set to first element in the prime array (2). On the second run, the item variable will be set to the second element in the prime array (3) and so on.
Go ahead and run the playbook to see all the elements of the prime list displayed:
[elliot@control plays]$ ansible-playbook print-list.yml
PLAY [print list] **************************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Show first five prime numbers] *******************************************
ok: [node1] => (item=2) => {
"msg": 2
}
ok: [node1] => (item=3) => {
"msg": 3
}
ok: [node1] => (item=5) => {
"msg": 5
}
ok: [node1] => (item=7) => {
"msg": 7
}
ok: [node1] => (item=11) => {
"msg": 11
}
PLAY RECAP *********************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0
Now you apply loops to a real life application. For example, you can create an add-users.yml playbook that would add multiple users on all the hosts in the dbservers group:
[elliot@control plays]$ cat add-users.yml
---
- name: Add multiple users
hosts: dbservers
vars:
dbusers:
- username: brad
pass: pass1
- username: david
pass: pass2
- username: jason
pass: pass3
tasks:
- name: Add users
user:
name: "{{ item.username }}"
password: "{{ item.pass | password_hash('sha512') }}"
loop: "{{ dbusers }}"
I first created a dbusers list which is basically a list of hashes/dictionaries. I then used the user module along with a loop to add the users and set the passwords for all users in the dbusers list.
Notice that I also used the dotted notation item.username and item.pass to access the keys values inside the hashes/dictionaries of the dbusers list.
It is also worth noting that I used the password_hash('sha512') filter to encrypt the user passwords with the sha512 hashing algorithm as the user module wouldn’t allow setting unencrypted user passwords.
Now let’s run the add-users.yml playbook:
[elliot@control plays]$ ansible-playbook add-users.yml
PLAY [Add multiple users] ******************************************************
TASK [Gathering Facts] *********************************************************
ok: [node4]
TASK [Add users] ***************************************************************
changed: [node4] => (item={'username': 'brad', 'pass': 'pass1'})
changed: [node4] => (item={'username': 'david', 'pass': 'pass2'})
changed: [node4] => (item={'username': 'jason', 'pass': 'pass3'})
PLAY RECAP *********************************************************************
node4 : ok=2 changed=1 unreachable=0 failed=0 skipped=0
You can verify that the three users are added by following up with an Ansible ad-hoc command:
[elliot@control plays]$ ansible dbservers -m command -a "tail -3 /etc/passwd"
node4 | CHANGED | rc=0 >>
brad:x:1001:1004::/home/brad:/bin/bash
david:x:1002:1005::/home/david:/bin/bash
jason:x:1003:1006::/home/jason:/bin/bash
Looping over dictionaries
You can only use loop with lists. You will get an error if you try to loop over a dictionary.
For example, if you run the following print-dict.yml playbook:
[elliot@control plays]$ cat print-dict.yml
---
- name: Print Dictionary
hosts: node1
vars:
employee:
name: "Elliot Alderson"
title: "Penetration Tester"
company: "Linux Handbook"
tasks:
- name: Print employee dictionary
debug:
msg: "{{ item }}"
loop: "{{ employee }}"
You will get the following error:
[elliot@control plays]$ ansible-playbook print-dict.yml
PLAY [Print Dictionary] ********************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Print employee dictionary] ***********************************************
fatal: [node1]: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list, got this instead: {'name': 'Elliot Alderson', 'title': 'Penetration Tester', 'company': 'Linux Handbook'}. 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."}
As you can see the error, it explicitly says that it requires a list.
To fix this error; you can use the dict2items filter to convert a dictionary to a list. So, in the print-dict.yml playbook; edit the line:
loop: "{{ employee }}"
and apply the dict2items filter as follows:
loop: "{{ employee | dict2items }}"
Then run the playbook again:
[elliot@control plays]$ ansible-playbook print-dict.yml
PLAY [Print Dictionary] ********************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Print employee dictionary] ***********************************************
ok: [node1] => (item={'key': 'name', 'value': 'Elliot Alderson'}) => {
"msg": {
"key": "name",
"value": "Elliot Alderson"
}
}
ok: [node1] => (item={'key': 'title', 'value': 'Penetration Tester'}) => {
"msg": {
"key": "title",
"value": "Penetration Tester"
}
}
ok: [node1] => (item={'key': 'company', 'value': 'Linux Handbook'}) => {
"msg": {
"key": "company",
"value": "Linux Handbook"
}
}
PLAY RECAP *********************************************************************
node1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0
Success! The key/value pairs of the employee dictionary were displayed.
Looping over a range of numbers
You can use the range() function along with the list filter to loop over a range of numbers.
For example, the following task would print all the numbers from 0-9:
- name: Range Loop
debug:
msg: "{{ item }}"
loop: "{{ range(10) | list }}"
You can also start your range at a number other than zero. For example, the following task would print all the numbers from 5-14:
- name: Range Loop
debug:
msg: "{{ item }}"
loop: "{{ range(5,15) | list }}"
By default, the stride is set to 1. However, you can set a different stride.
For example, the following task would print all the even IP addresses in the 192.168.1.x subnet:
- name: Range Loop
debug:
msg: 192.168.1.{{ item }}
loop: "{{ range(0,256,2) | list }}"
Where start=0, end<256, and stride=2.
Looping over inventories
You can use the Ansible built-in groups variable to loop over all your inventory hosts or just a subset of it. For instance, to loop over all your inventory hosts; you can use:
loop: "{{ groups['all'] }}"
If you want to loop over all the hosts in the webservers group, you can use:
loop: "{{ groups['webservers'] }}"
To see how this works in playbook; take a look at the following loop-inventory.yml playbook:
[elliot@control plays]$ cat loop-inventory.yml
---
- name: Loop over Inventory
hosts: node1
tasks:
- name: Ping all hosts
command: ping -c 1 "{{ item }}"
loop: "{{ groups['all'] }}"
This playbook tests if node1 is able to ping all other hosts in your inventory. Go ahead and run the playbook:
[elliot@control plays]$ ansible-playbook loop-inventory.yml
PLAY [Loop over Inventory] *****************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Ping all hosts] **********************************************************
changed: [node1] => (item=node1)
changed: [node1] => (item=node2)
changed: [node1] => (item=node3)
changed: [node1] => (item=node4)
PLAY RECAP *********************************************************************
node1 : ok=2 changed=1 unreachable=0 failed=0 skipped=0
If you get any errors; this would mean that your managed hosts are not able to ping (reach) each other.
Pausing within loops
You may want to pause for a certain amount of time between each loop iteration. To do this, you can use the pause directive along with the loop_control keyword.
To demonstrate, let’s write a countdown.yml playbook that would simply do a ten seconds countdown before displaying the message “Happy Birthday!” on the screen:
[elliot@control plays]$ cat countdown.yml
---
- name: Happy Birthday Playbook
hosts: node1
tasks:
- name: Ten seconds countdown
debug:
msg: "{{ 10 - item }} seconds remaining ..."
loop: "{{ range(10) | list }}"
loop_control:
pause: 1
- name: Display Happy Birthday
debug:
msg: "Happy Birthday!"
Go ahead and run the playbook:
[elliot@control plays]$ ansible-playbook countdown.yml
PLAY [Happy Birthday Playbook] *************************************************
TASK [Gathering Facts] *********************************************************
ok: [node1]
TASK [Wait for ten seconds] ****************************************************
ok: [node1] => (item=0) => {
"msg": "10 seconds remaining ..."
}
ok: [node1] => (item=1) => {
"msg": "9 seconds remaining ..."
}
ok: [node1] => (item=2) => {
"msg": "8 seconds remaining ..."
}
ok: [node1] => (item=3) => {
"msg": "7 seconds remaining ..."
}
ok: [node1] => (item=4) => {
"msg": "6 seconds remaining ..."
}
ok: [node1] => (item=5) => {
"msg": "5 seconds remaining ..."
}
ok: [node1] => (item=6) => {
"msg": "4 seconds remaining ..."
}
ok: [node1] => (item=7) => {
"msg": "3 seconds remaining ..."
}
ok: [node1] => (item=8) => {
"msg": "2 seconds remaining ..."
}
ok: [node1] => (item=9) => {
"msg": "1 seconds remaining ..."
}
TASK [Display Happy Birthday] **************************************************
ok: [node1] => {
"msg": "Happy Birthday!"
}
PLAY RECAP *********************************************************************
node1 : ok=3 changed=0 unreachable=0 failed=0 skipped=0
So, you get familiar with loops in Ansible. Stay tuned for next tutorial as you are going to learn how to add decision-making skills to your Ansible playbooks.
If you have not already, consider becoming a pro member of Linux Handbook for more ad-free, reading experience with independent publishers like us. You may always support us on Buy Me a Coffee.