While working on my Ansible Galaxy users role I came across a situation where I needed to loop through a list inside a dictionary inside a list. For this specific case I had a list of users, and each user could have multiple authorized sshkeys stored in a “pubkeys” value. To add each sshkey I used subelements in Ansible to loop through multiple lists.
Here’s an example variable. Note that there are two users, one user has two public keys, and the other has one:
host_local_users:
- name: user1
pubkeys:
- 'ssh-rsa myr4nd0mk3y engonzal@home'
- 'ssh-rsa my000r4nd0m000k3y000tw0 engonzal@workstation'
- name: user2
pubkeys:
- 'ssh-rsa myr4nd0mk3y engonzal@home'
Creating a Task
In this case “host_local_users” is a list. Each item of “host_local_users” is a dictionary. The key “pubkeys” is a list. Now say we want to have a task that adds each key under pubkeys for the user in “name”. That would look something like this:
- name: Add public keys to authorized_hosts
authorized_key:
user: "{{ item.0.name }}"
state: "{{ item.0.state | default('present') }}"
key: "{{ item.1 }}"
loop: "{{ host_local_users | subelements('pubkeys', 'skip_missing=True') }}"
There’s a lot going on in this one task! First note that the “loop” parameter is using our “host_local_users” variable. We’re using the “subelements” filter to pick out the list of ssh keys for each user. Each item of host_local_users is represented by “item.0”. Each item of pubkeys is represented by “item.1” (note that we provided ‘pubkeys’ to the “subelements” filter).
Running the task
In practice this will run the authorized_key task 3 times even though there are only two users, it looks like this:
TASK [engonzal.users : Add public keys to authorized_hosts] ********************
changed: [instance] => (item=[{u'pubkeys': [u'ssh-rsa myr4nd0mk3y engonzal@home', u'ssh-rsa my000r4nd0m000k3y000tw0 engonzal@workstation'], u'name': u'user1'}, u'ssh-rsa myr4nd0mk3y engonzal@home'])
changed: [instance] => (item=[{u'pubkeys': [u'ssh-rsa myr4nd0mk3y engonzal@home', u'ssh-rsa my000r4nd0m000k3y000tw0 engonzal@workstation'], u'name': u'user1'}, u'ssh-rsa my000r4nd0m000k3y000tw0 engonzal@workstation'])
changed: [instance] => (item=[{u'pubkeys': [u'ssh-rsa myr4nd0mk3y engonzal@home'], u'name': u'user2'}, u'ssh-rsa myr4nd0mk3y engonzal@home'])
This run added two ssh pubkeys for user1 and one ssh pubkey for user2 based on what we put in the variable “host_local_users”.
I thought it was pretty cool that Ansible allowed these types of complex variables. Many would argue that for some problems it makes more sense to use bash or a programming language for complicated tasks. I’ve found that making those scripts idempotent adds a whole other layer of complexity that Ansible already has baked in.
To get Ansible setup checkout my post on how to install Ansible using virtualenvs. To see subelements in practice take a look at the tasks in my “users” role on Github.