Ansible - Looping Over Lists and Dictionaries

Recently I’ve been writing some Ansible plays for a personal project and looking around online reminded me just how much people struggle with handling loops. It’s not a huge surprise, whilst the documentation is pretty clear it’s written in a slightly abstract way that can a little difficult to absorb if you’re a newcomer to Ansible, this isn’t aided by the fact that there are several options for looping and they all get muddled up together. In this very short post I’m going to take a look at how to handle simple looping.

One Function; loop

In the early days of Ansible, the only option to iterate over items was using the with_items and with_list functions, I’ve never been a big fan and their uses can get pretty messy. As of Ansible 2.5 the loop function is stable and is the desired solution for all loops (though with remains a supported solution).

Let’s take a look at how loop works in a simple debug operation:

vars:
example_list_of_names:
  - Alice
  - Bob
  - Carol
  - Ted

tasks:
- name: Print out all user names
  debug:
    
    msg: "{{ item }}"
  loop: "{{ example_list_of_names }}"

In the above example, the loop function reads the contents of the example_list_of_users list and with each iteration passes the current value to the iterator ”{{ item }}”. When executing we should then simply see all 4 names printed out:

TASK [Print out all user names] *********************************************************************************************************
ok: [localhost] => (item=Alice) => {
    "msg": "Alice"
}
ok: [localhost] => (item=Bob) => {
    "msg": "Bob"
}
ok: [localhost] => (item=Carol) => {
    "msg": "Carol"
}
ok: [localhost] => (item=Ted) => {
    "msg": "Ted"
}

So far this is analogous to using with_items or with_list, so let’s try and do something a little more advanced.

Looping Over Dictionaries

Ideally the goal when creating Plays should be to write a single templated Task and have it be reusable via input variables. In the below example we’re going to provide a set of variables to create new local users on a remote system, providing the values as a simple dictionary:

#--user_data.yaml

user_configs:
  alice:
    shell: /bin/bash    
    group: admin_users
  bob:
    shell: /bin/bash    
    group: standard_users
  carol:
    shell: /bin/zsh
    group: standard_users

These variables can then be consumed by a Task as shown below:

---
- name: Loop Example
  hosts: test_node
  gather_facts: false
  vars_files:
    - ../vars/user_data.yaml
  tasks:

  - name: Create Accounts
    user:
      name: "{{ item.key }}"
      comment: "{{ item.key }}"
      home: "/home/{{ item.key }}"
      shell: "{{ item.value.shell }}"
      group: "{{ item.value.group }}"
    loop: "{{ lookup('dict', user_configs) }}"
...

In this loop, the lookup plugin is used and each Key: Value pair in the dictionary is iterated over. For example the first item’s Key is Alice which can be called with ” {{ item.key }}”, this item has two child Values of Shell and Group, each of which can be called with ”{{ item.value.shell }}” and ”{{ item.value.group }}” respectively.

Ultimately, this Task will produce our 3 new accounts (and any number of accounts we need) just by altering the input values:

PLAY [Loop Example] *********************************************************************************************************************

TASK [Create Accounts] ******************************************************************************************************************
ok: [localhost] => (item={u'value': {u'shell': u'/bin/bash', u'group': u'admin_users'}, u'key': u'carol'})
ok: [localhost] => (item={u'value': {u'shell': u'/bin/bash', u'group': u'standard_users'}, u'key': u'bob'})
ok: [localhost] => (item={u'value': {u'shell': u'/bin/zsh', u'group': u'standard_users'}, u'key': u'alice'})

PLAY RECAP ******************************************************************************************************************************
test_node   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
Written on April 6, 2022