Loops¶
What You'll Learn¶
- The
loop:keyword and theitemvariable - Looping over a list of dictionaries
- Why
loopreplacedwith_itemsin modern playbooks
Minimal Example¶
- name: Install a list of packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
- git
Runs the task three times, once per list item, with item bound to the current value each time.
Looping Over a List of Dictionaries¶
- name: Create application users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: /bin/bash
loop:
- { name: deploy, groups: sudo }
- { name: appuser, groups: docker }
Renaming the Loop Variable¶
- name: Ensure config directories exist
ansible.builtin.file:
path: "/etc/app/{{ dir_name }}"
state: directory
loop:
- conf.d
- templates
loop_control:
loop_var: dir_name
loop_control.loop_var matters when a loop is inside an include_tasks that itself contains another loop — without renaming, the inner loop's item would shadow the outer one.
Keeping Output Readable: label¶
By default, Ansible prints the whole item for every iteration. For a list of dictionaries that's noisy, and if an item contains a password or token it's a leak:
- name: Create database users
community.postgresql.postgresql_user:
name: "{{ item.name }}"
password: "{{ item.password }}"
loop: "{{ db_users }}"
loop_control:
label: "{{ item.name }}" # prints (item=checkout), not the whole dict
no_log: true
label only changes what's displayed; no_log is still what keeps the secret out of results and logs.
Retrying Until Something Is Ready: until¶
A different kind of loop: repeat one task until a condition holds. This is the standard way to wait for a service after a restart, or for an API to report a job finished:
- name: Wait for the app to report healthy after the restart
ansible.builtin.uri:
url: http://localhost:8080/healthz
return_content: true
register: health
until: health.status == 200 and (health.json.status | default('')) == 'ok'
retries: 20
delay: 3 # 20 × 3 s = give up after about a minute
The task fails only if the condition is still false after the last retry. To wait for a port to open, ansible.builtin.wait_for: port=8080 is simpler.
loop vs. Legacy with_*¶
# Legacy — still works, avoid in new playbooks
- ansible.builtin.package:
name: "{{ item }}"
with_items:
- nginx
- curl
# Modern — use this
- ansible.builtin.package:
name: "{{ item }}"
loop:
- nginx
- curl
with_items and its siblings (with_dict, with_fileglob, ...) were Ansible's original looping mechanism, each backed by a different "lookup plugin" with its own quirks. loop (paired with a Jinja2 filter when you need with_dict-style behavior — loop: "{{ my_dict | dict2items }}") is the single, more predictable modern replacement. Both still work; only loop should appear in new code.
Common Mistakes¶
- Using
with_items/with_dictin new playbooks out of habit or copied examples. - Looping over hundreds of items expecting parallelism — by default, loop iterations run sequentially within a single host/task; see Async and Poll for genuinely parallel per-item work.
- Nesting loops without renaming
loop_var, causing the inner loop to silently overwrite the outeritem. - Looping over a list of dictionaries that contain secrets without
no_log, so every iteration prints them. - Installing packages one per loop iteration.
ansible.builtin.packageandapt/dnfaccept a list inname:and install everything in one transaction, which is much faster.
Interview Questions¶
- What's the difference between
loopandwith_items? - Does a
looprun its iterations in parallel or sequentially by default? - When would you use
loop_control.loop_var?
Next¶
Continue to Handlers.