Ansible

RHCE Ansible Series #5: Ansible Loops

This is the fifth chapter of RHCE Ansible EX 294 exam preparation series. And in this, you'll learn about using loops in Ansible. The tutorial will be available to public after a week. Become a free member to access it today.

Ahmed Alkabary
Ahmed Alkabary

Table of Contents

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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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.

RHCE Exam Tip: You will have access to the docs.ansible.com page on your exam. It a very valuable resource, especially under the “Frequently Asked Questions” section; you will find numerous How-to questions with answers and explanations.

Now let’s run the add-users.yml playbook:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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:

[[email protected] 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.



Join the conversation.