Chapter 5: Implementing Task Control

Writing loops

Five users to create means five nearly identical tasks, unless you use a loop. A loop runs one task once for each item in a list, so the playbook stays short and adding a sixth user is a one-line change.

Simple loops

Add the loop keyword to a task and give it a list. Ansible runs the task once per list entry, and during each run the variable item holds the current entry.

- name: Postfix is running
  ansible.builtin.service:
    name: postfix
    state: started

- name: Dovecot is running
  ansible.builtin.service:
    name: dovecot
    state: started
- name: Postfix and Dovecot are running
  ansible.builtin.service:
    name: "{{ item }}"
    state: started
  loop:
    - postfix
    - dovecot

loop is a task keyword, so it sits at the task level, aligned with the module name, not inside the module's arguments.

Add and remove items below, and switch to a list of dictionaries, to see how one task becomes several runs:

Looping over a variable

The list can come from a variable instead of being written in the task. Because the value then starts with {{, quote it:

vars:
  mail_services:
    - postfix
    - dovecot

tasks:
  - name: Postfix and Dovecot are running
    ansible.builtin.service:
      name: "{{ item }}"
      state: started
    loop: "{{ mail_services }}"

Looping over a list of dictionaries

Each list entry can be a dictionary. item is then the whole dictionary, and you read its keys with item['key']:

- name: Users exist and are in the correct groups
  ansible.builtin.user:
    name: "{{ item['name'] }}"
    state: present
    groups: "{{ item['groups'] }}"
  loop:
    - name: jane
      groups: wheel
    - name: joe
      groups: root

The result: jane exists and is in wheel, and joe exists and is in root.

Older loop keywords

Before Ansible 2.5, loops were written with keywords that start with with_, followed by the name of a lookup plug-in. You will still meet them in older playbooks and roles:

KeywordBehaviour
with_itemsLike loop for simple lists. Unlike loop, if you give it a list of lists, it flattens them into one list first.
with_fileTakes a list of file names on the control node. item holds the content of each file in turn.
with_sequenceGenerates a list from a numeric sequence. item holds each generated number.
vars:
  data:
    - user0
    - user1
    - user2
tasks:
  - name: "with_items"
    ansible.builtin.debug:
      msg: "{{ item }}"
    with_items: "{{ data }}"

Registering the results of a loop

register works on a looping task too, but the variable's shape changes. Instead of one result, it holds a results list with one entry per item, and each entry records which item it belongs to.

- name: Loop Register Test
  gather_facts: false
  hosts: localhost
  tasks:
    - name: Looping Echo Task
      ansible.builtin.shell: "echo This is my item: {{ item }}"
      loop:
        - one
        - two
      register: echo_results

    - name: Show echo_results variable
      ansible.builtin.debug:
        var: echo_results

Explore the registered variable (trimmed to the most useful keys):

To use each result, loop over the results list. Inside that loop, item is one result dictionary:

- name: Show stdout from the previous task
  ansible.builtin.debug:
    msg: "STDOUT from previous task: {{ item['stdout'] }}"
  loop: "{{ echo_results['results'] }}"
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: one', ...}) => {
    "msg": "STDOUT from previous task: This is my item: one"
}
ok: [localhost] => (item={'changed': True, 'stdout': 'This is my item: two', ...}) => {
    "msg": "STDOUT from previous task: This is my item: two"
}

Takeaway

Loop a task over a list with loop and item. Use the chapter lab to check this on a real host.