Looping in Ansible with_items

There are many types of loop within Ansible, but by far the most common is the 'standard loop', or the with_items loop.

Curiously, the with_items loop doesn't look much like a loop to the untrained eye. Certainly, syntactically (what a word) it's far removed from for and foreach.

The syntax for a standard / with_items loop looks like this:

- name: Install common software requirements
  apt: pkg={{ item }} state=installed
     - git
     - htop
     - vim

When a Playbook is run, the above task would be interpreted as three separate steps - which is to say that whilst Ansible's output would show only one task having been run, there would be three changed outcomes, one per item installed.

Because we have used quite a simple loop here, Ansible would realise that the output of the entire loop could be inlined.

Perhaps easier to see than to explain:

* cut *

TASK: [Install common software requirements] ***
changed: [] => (item=git,htop,vim)

* cut *

As you'll see in the video, we can make use of the with_items style looping in more involved tasks, combining with other features we have already learned about - variables in this case.

The difference here is that Ansible usually outputs the results of a more complicated loop like this onto one line per item.

Again, perhaps easier to show than to explain - and you will see this in the video around the 4:00 minute mark:

* cut *

TASK: [Create home directory folder structure] ***
changed: [] => (item=src)
changed: [] => (item=backups)
changed: [] => (item=bin)

* cut *

Again, it's showing the same output, but this time one per line.

More Complex Loops

Whilst the with_items style loops are very handy, and very nice to have, you will likely need something more complex as you spend more time with Ansible.

Fortunately, there are many looping options to choose from.

To begin with, we can make our Standard Loop (with_items) more powerful by passing in a list of hashes. This gives us the ability to name our keys inside our hashes, and then reference those names inside our loop.

Again, that probably sounds more complex than it need be, so let's take a look at the example from the Ansible Docs:

- name: add several users
  user: name={{ item.name }} state=present groups={{ item.groups }}
    - { name: 'testuser1', groups: 'wheel' }
    - { name: 'testuser2', groups: 'root' }

We already know pretty much everything that's going on here. The only difference being that we now have access to named subkeys, instead of just having {{ item }}.

This really only scratches the surface of looping.

Nested Loops give you loops inside loops. A handy use for nested loops would be creating the home directory structure for multiple users. All you need to do is specify a list of users, and a list of directories, and let with_nested handle the rest.

There are more loops than I could cover here, and some really only apply to very specific situations.

I would strongly advise you read the docs and get a feel for all the looping options available:

  • with_dict
  • with_fileglob
  • with_subelements
  • with_sequence
  • with_random_choice

These are just a few of what's available, and you can create your own custom looping mechanics on the off-chance that Ansible doesn't provide what you need.

Looping With Dynamic Items

Perhaps a little more realistic / real-world is the situation where you need to pass in some variables from your host_vars/ or group_vars/ directory to the with_items loop.

Let's say I have a playbook that sets up a few website directories. I want to keep this step near the rest of my nginx playbook entries, so define an 'inline' set of tasks as follows:

# /playbook/nginx.yml

- hosts: nginx_servers
  sudo: True

    - name: "check for existence of website directory - {{ item.name }}"
      stat: path="/var/www/{{ item.directory }}"
      register: "website_directory_exists"
      with_items: "{{ website_directories }}"

    # - debug: var=website_directory_exists

    # - debug: msg="item.item={{item.item}}, item.changed={{item.changed}}"
    #  with_items: "{{website_directory_exists.results}}"

    - name: "create website directory - {{ item.item.name }}"
      file: dest="/var/www/{{ item.item.directory }}" 
            owner="{{ nginx_user }}" 
            group="{{ nginx_group }}" 
      when: item.stat.exists == False
      with_items: "{{ website_directory_exists.results }}"

And in my specific host_vars/ or group_vars/ directory, I might have e.g.:

# /host_vars/somehost.yml

website_domain_name: "api.mysite.com"

  - { name: "API", directory: "{{ website_domain_name }}" } 
  - { name: "Demo", directory: "some-demo-path" } 
  - { name: "Another", directory: "another-why-not" } 

What's nice about this setup is that I can pass in a simple list of object hashes (website_directories list), which in itself can contain entries that have variables (e.g. the API hash).

This list can be passed through to my playbook, which checks if the dir exists, and if not, goes about creating one and setting up some user, group, and permission settings.

I've left the debug statements in which helped me figure this problem out. I found those courtesy of Kashyap's stackoverflow answer.

The only part I don't like is the item.item.name, but I can't see a way around that at this stage. Ansible's internal naming for this seems strange. Still, it works well, just maybe worth leaving a comment that it's not a mistake, just for your future overly-eager-refactoring self.

Code For This Course

Get the code for this course.