
One of the real strengths of Ansible is it’s ability to loop through information. This can be done to generate reports or configuring network or server kit.
The aim of this post is teach you how to loop over various data types be them lists(which are easy) or dictionaries(a little more challenging). Another important piece here is the fact that often I need to do some advanced filtering. This could be done with complex nested loops, OR I can do it with some special plugins(which will be demonstrated).
Sample Files
My public git repository with the files can be found here.
If you are in a workshop you can issue the following command to clone the repository to your workstation in your current working directory:
1 | git clone https://github.com/gregsowell/ansible-looping-lesson.git |
Looping Over Lists
The file I’m pulling from is loop-test.yml. I’ll break down the various parts below.
The play section and variables I’m working with are as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | ---
- name: loop examples
hosts: localhost
gather_facts: false
vars:
list1:
- thing1
- thing2
- thing3
list2:
- fname: Greg
lname: Sowell
- fname: Andy
lname: Garrett
list3:
- 1:
- n: 1
- m: 1
- 2:
- n: 2
- m: 2 |
Looking at the above, “list1” variable is a standard list. It has just values as the items inside the list.
“list2” is actually a list of dictionaries. List items begin with a “-” character. Each list item has two variables, both “fname” and “lname”.
“list3 is a list of lists.
You will often see variables returned in ansible in these forms. I would say I see combinations of list2 and list3 most often…probably list2 the most often.
Here are two tasks that will display list1 in different ways:
1 2 3 4 5 6 7 8 | - name: debug list1
ansible.builtin.debug:
var: list1
- name: loop through standard list
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ list1 }}" |
Here’s the returned format once they are run:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | TASK [debug list1] *************************************************************
ok: [localhost] => {
"list1": [
"thing1",
"thing2",
"thing3"
]
}
TASK [loop through standard list] **********************************************
ok: [localhost] => (item=thing1) => {
"msg": "thing1"
}
ok: [localhost] => (item=thing2) => {
"msg": "thing2"
}
ok: [localhost] => (item=thing3) => {
"msg": "thing3"
} |
The initial debug isn’t that complex; all I did was request that variable 1 be dumped out to screen.
The second debug where I “loop through standard list” is slightly more interesting. I added a “loop” option with the “list1 variable. Notice the format:
1 | loop: "{{ list1 }}" |
Have a look at how the word loop is on the same column level as the module itself. This says that the loop option is applied to the task itself and is not a sub parameter of the module.
Also notice, in the line below, that the result of the loop is contained in a temporary variable named “item”. This is the default behavior, though the returned iteration name “item” can be changed to something else if you want to.
1 | msg: "{{ item }}" |
Picking specific items inside the array
A list is like an array. You can pick specific values like so:
1 2 3 | - name: pulling items from list 1
ansible.builtin.debug:
msg: "{{ list1[1] }}" |
This returns the following results:
1 2 3 4 | TASK [pulling items from list 1] ***********************************************
ok: [localhost] => {
"msg": "thing2"
} |
This list contained 3 items and I specified 1, so why did it return thing2 and not thing1? That’s because ansible, like most programming languages, numbers its arrays starting at 0. So for this list it goes 0, then 1, and last 2.
Loop through lists of dictionaries
Looping through lists of dictionaries is virtually the same as before, but with more options. See the following example:
1 2 3 4 | - name: loop through list of dictionaries
ansible.builtin.debug:
msg: "First name is:{{ item.fname }} last name is:{{ item.lname }}"
loop: "{{ list2 }}" |
Notice the loop section is the same, and it returns iterations as “item” just the same, but look at the variables in the “msg” option…not quite the same.
The contents of item for each iteration will be the dictionary key/value pairs. This allows me to reference the contents of the variable, so in each list item I have a fname and a lname. So to reference each I say item.fname or item.lname.
Here are the results of the run:
1 2 3 4 5 6 7 | TASK [loop through list of dictionaries] ***************************************
ok: [localhost] => (item={'fname': 'Greg', 'lname': 'Sowell'}) => {
"msg": "First name is:Greg last name is:Sowell"
}
ok: [localhost] => (item={'fname': 'Andy', 'lname': 'Garrett'}) => {
"msg": "First name is:Andy last name is:Garrett"
} |
Looping Over Dictionaries
This gets a little harrier. I say that because ansible won’t natively loop over a dictionary. The trick is, I need to convert the dictionary into a list. I’m specifically going to be focusing on the “dict2items” plugin. This will take that dictionary and create key/value pairs with it. Often the easiest way to understand it is an example.
Task:
1 2 3 | - name: debug dict3 with dict2items
ansible.builtin.debug:
var: dict3 | dict2items |
Dictionary variable I am manipulating:
1 2 3 4 5 6 7 8 9 10 | dict3:
1:
name: New Greg
color: Bright
2:
name: Andy
color: Green
3:
name: Andrew
color: Baby blue |
Now the results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | TASK [debug dict3 with dict2items] *********************************************
ok: [localhost] => {
"dict3 | dict2items": [
{
"key": 1,
"value": {
"color": "Bright",
"name": "New Greg"
}
},
{
"key": 2,
"value": {
"color": "Green",
"name": "Andy"
}
},
{
"key": 3,
"value": {
"color": "Baby blue",
"name": "Andrew"
}
}
]
} |
First, notice in the original dictionary variable, there are no “-“s. That shows you that it is, in fact, not a list. Remember, a list has dashes in standard variable form.
Second, take a look at the output…there are no “-” in there, so how is it a list? In json output a list is surrounded by square brackets like so “[]”. If you notice at the very beginning and very end of the output you can see the square brackets! So, dashes in defined variables and square brackets in output means list.
Now for the dict2items output. I see that it breaks the original dictionary into two sections, first a key and then a value.
I can see that the left most part of the dictionary column becomes the “key”. So in this example it’s 1, 2, and 3.
Next, the indented information to the right of the key gets put into the “value” section.
I can actually access the individual pieces under the value section…let me show you an example:
1 2 3 4 | - name: loop through dictionary with many entries
ansible.builtin.debug:
msg: "What is the key:{{ item.key }}. What is the name:{{ item.value.name }}. => {{ item.value.color }}"
loop: "{{ dict3 | dict2items }}" |
Notice in the above that inside the loop section I piped the dictionary over to the dict2items plugin right there; I don’t have to do this ahead of time, rather I can do it right when I need it.
Also notice that in the returned info I referenced the key directly with “item.key” and then referenced the value information directly by following the variable chain with “item.value.name”.
Imagine that any time I’m moving a column to the right to access another variable name, just add a “.” between the variable names. So looking above where I debugged the dict2items I can pretty easily see myself moving to the right and just add periods.
Results:
1 2 3 4 5 6 7 8 9 10 | TASK [loop through dictionary with many entries] *******************************
ok: [localhost] => (item={'key': 1, 'value': {'name': 'New Greg', 'color': 'Bright'}}) => {
"msg": "What is the key:1. What is the name:New Greg. => Bright"
}
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
"msg": "What is the key:2. What is the name:Andy. => Green"
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
"msg": "What is the key:3. What is the name:Andrew. => Baby blue"
} |
Advanced Filtering with selectattr
Additional resources here and here.
There are MANY ways to do additional filtering, but what I’m going to focus on is using the “selectattr” plugin. This plugin takes a list of dictionaries and applies a “test” to each iteration in the list.
It’s utilization is something like the following:
1 | selectattr('key','search','^Ethernet.*') |
So selectattr(field to be evaluated, jinja2 test, optional value to add to the test).
There are several options for the test(some examples follow, but there are more): search, match(search and match are pretty much the same), eq, ==(eq and == are the same), >, <, >=, <=, !=, defined.
Let's see a few examples based on some of this variable:
1 2 3 4 5 6 7 8 9 10 | dict3:
1:
name: New Greg
color: Bright
2:
name: Andy
color: Green
3:
name: Andrew
color: Baby blue |
Equals or not equals
Tasks:
1 2 3 4 5 6 7 8 9 | - name: loop through dict3 selectattr == 1
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | selectattr('key','==',1) }}"
- name: loop through dict3 selectattr != 1
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | selectattr('key','!=',1) }}" |
Notice here I’m specifying that they key field either matches 1 in the first task or doesn’t match 1 in the second task.
Results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | TASK [loop through dict3 selectattr == 1] **************************************
ok: [localhost] => (item={'key': 1, 'value': {'name': 'New Greg', 'color': 'Bright'}}) => {
"msg": {
"key": 1,
"value": {
"color": "Bright",
"name": "New Greg"
}
}
}
TASK [loop through dict3 selectattr != 1] **************************************
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
"msg": {
"key": 2,
"value": {
"color": "Green",
"name": "Andy"
}
}
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
"msg": {
"key": 3,
"value": {
"color": "Baby blue",
"name": "Andrew"
}
}
} |
Regex searching
Task:
1 2 3 4 | - name: loop through dict3 selectattr search for A in value.name
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | selectattr('value.name','search','^A.*') }}" |
This one is pretty darn handy. It allows you to do a regex search on any field, so it will narrow down what results are returned based on what you match.
Results:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | TASK [loop through dict3 selectattr search for A in value.name] ****************
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
"msg": {
"key": 2,
"value": {
"color": "Green",
"name": "Andy"
}
}
}
ok: [localhost] => (item={'key': 3, 'value': {'name': 'Andrew', 'color': 'Baby blue'}}) => {
"msg": {
"key": 3,
"value": {
"color": "Baby blue",
"name": "Andrew"
}
}
} |
Compound tests
Yes, as with any filter plugin you can stack them to check for multiple conditions. I’ll take the example from before and build on it.
Task:
1 2 3 4 | - name: loop through dict3 selectattr search for A in value.name and like green
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | selectattr('value.name','search','^A.*') | selectattr('value.color','eq','Green') }}" |
It is as simple as adding another pipe and then the next test. Notice too that I checked different variables in each test.
Results:
1 2 3 4 5 6 7 8 9 10 | TASK [loop through dict3 selectattr search for A in value.name and like green] ***
ok: [localhost] => (item={'key': 2, 'value': {'name': 'Andy', 'color': 'Green'}}) => {
"msg": {
"key": 2,
"value": {
"color": "Green",
"name": "Andy"
}
}
} |
Make sure the variable is defined
The selectattr plugin will fail if one of the entries happens to not have the field to be checked not exist, so I can first check defined, then build on that as follows.
Task:
1 2 3 4 | - name: loop through dict3 selectattr search for A in value.name and check that it is defined
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | selectattr('value.name','defined') | selectattr('value.name','search','^A.*') }}" |
So in this example, it will first check that value.name exists for this host, and if it does, then it will do a regex search on it. You don’t necessarily need to do this for every variable you plan to check, but if there’s a chance it may not be instantiated, then add it; it won’t hurt to have it and not need it.
Use jinja2 loop to build a variable
Here I’m breaking into a jinja2 loop in conjunction with selectattr to build a custom variable:
1 2 3 4 5 6 7 8 9 | - name: create variable in a jinja2 loop using selectattr
ansible.builtin.set_fact:
new_var: "{% for result in dict3 | dict2items | selectattr('value.name','defined') | selectattr('value.name','search','^A.*') %}\
{{ result.key }}: {{ result.value.name }}_{{ result.value.color }},
{% endfor %}"
- name: print out new_var
ansible.builtin.debug:
var: new_var.split(',') |
Notice that in the debug statement I’m using the split option to break the string into a list separated via a comma.
Results:
1 2 3 4 5 6 7 8 9 10 11 | TASK [create variable in a jinja2 loop using selectattr] ***********************
ok: [localhost]
TASK [print out new_var] *******************************************************
ok: [localhost] => {
"new_var.split(',')": [
"2: Andy_Green",
" 3: Andrew_Baby blue",
" "
]
} |
Advanced Filtering with map
Additional resources on map here and here.
The map plugin allows me to take a list of dictionaries and make a simpler list from it. Let’s jump into some examples.
Tasks:
1 2 3 4 | - name: loop through dict3 using map to create a list of names
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ dict3 | dict2items | map(attribute='value.name') }}" |
Results:
1 2 3 4 5 6 7 8 9 10 | TASK [loop through dict3 using map to create a list of names] ******************
ok: [localhost] => (item=New Greg) => {
"msg": "New Greg"
}
ok: [localhost] => (item=Andy) => {
"msg": "Andy"
}
ok: [localhost] => (item=Andrew) => {
"msg": "Andrew"
} |
So as you can see it simply returns a list of values, not key and value.
Say, for example, I wanted to return it all on a single line instead of a list I can do something like the following:
1 2 3 | - name: loop through dict3 using map to create a list of names all on one line
ansible.builtin.debug:
msg: "{{ dict3 | dict2items | map(attribute='value.name') | join(',') }}" |
I’m simply using the join plugin and instructing it to use a comma as the joining character.
Results:
1 2 3 4 | TASK [loop through dict3 using map to create a list of names all on one line] ***
ok: [localhost] => {
"msg": "New Greg,Andy,Andrew"
} |
Conclusion
This is a living document, so I plan to make updates periodically. I hope this gives you a starting point to build some awesome automations. If you have any questions or comments, please let me know.
Thanks and happy automated looping!
I saw this recently when helping to troubleshoot an issue “FIB did not return source address”. This was for a BGP route destination when I tried to traceroute it. The route was in the routing table, so why couldn’t I trace to it?
In BGP when I learn a route via eBGP(external BGP) the next-hop address for this route, by default, will always be the address of the eBGP neighbor. Even if I’m 5 iBGP routers away, it will always be the IP of the eBGP neighbor. This network build for this particular issue wasn’t so complicated.
Network diagram:
(eBGP ISP1) — (eBGP Border1 iBGP) — (iBGP Border2)
Border2 was trying to send traffic to destinations sourced from ISP1, but they were getting the FIB error. This is because while Border2 had the destinations learned via eBGP and were valid, they didn’t have a valid route to ISP1’s IP address(the one it was peered with and sourcing the routes from). They had a static null route for the ISP1 IP address on Border2 which means every route destined for ISP1 was getting thrown in the bit bucket.
Resolution is to ensure all routers have a valid route in their local table to reach the IP address that ISP1 is sourcing routes from. I generally do this by redistributing connected routes into OSPF. You could also explore something like “Next-hop self” which overrides the default behavior, but I prefer keeping it default so I can easily see where routes are sourced from.
Good luck and happy routing.
If you want to install Ansible Navigator or Ansible Builder on your machines, you really should be doing it from the Red Hat official files and not via PIP. The bundled Ansible Automation Platform installer includes the RPMs to install both navigator and builder. Here’s a quick and easy way to make the RPM directory a local repo.
Create the repo file(/etc/yum.repo.d/ansible-local.repo):
1 | vi /etc/yum.repo.d/ansible-local.repo |
1 2 3 4 5 | [ansible-local] name=ansible-local enabled=1 gpgcheck=0 baseurl=file:///root/ansible-automation-platform-setup-bundle-2.1.0-1/bundle/el8/repos |
In the above, you need to edit the baseurl to match your directory structure. Note that the baseurl needs to have three “/”s.
View the repolist to make sure our new repo shows up in the list:
1 | dnf repolist |
Install navigator:
1 | dnf install ansible-navigator |
If you want to install builder just do ansible-builder instead in the above.
If you have any questions or comments, please let me know!
Thanks and happy repo’ing!

