Introduction
loop repeats a task for each item in a list. It replaces the older with_items, with_dict, and with_fileglob keywords with a single, consistent syntax. Each iteration, the current item is available as {{ item }}.
Basic Loop
---
- name: Loop examples
hosts: all
become: true
tasks:
# Install multiple packages
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- redis
- postgresql-client
- curl
# More efficient — pass the entire list to apt
- name: Install packages (better)
ansible.builtin.apt:
name:
- nginx
- redis
- postgresql-client
state: present
Loop Over Dictionaries
# List of dicts
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
shell: "{{ item.shell | default('/bin/bash') }}"
loop:
- { name: alice, groups: "developers,docker" }
- { name: bob, groups: "developers,sudo" }
- { name: charlie, groups: "dbadmins" }
# Loop over dict key-value pairs
- name: Set sysctl values
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_set: true
loop: "{{ sysctl_settings | dict2items }}"
vars:
sysctl_settings:
net.ipv4.ip_forward: 1
vm.swappiness: 10
net.core.somaxconn: 65535
Loop Over a Variable
vars:
packages:
- nginx
- redis
- postgresql
users:
- name: alice
role: admin
- name: bob
role: developer
tasks:
- name: Install packages from variable
ansible.builtin.apt:
name: "{{ item }}"
loop: "{{ packages }}"
- name: Create users from variable
ansible.builtin.user:
name: "{{ item.name }}"
loop: "{{ users }}"
Loop Index
Access the loop index and other metadata with loop_var attributes:
- name: Show loop info
ansible.builtin.debug:
msg: >
Item {{ ansible_loop.index }} of {{ ansible_loop.length }}:
{{ item }}
loop:
- alpha
- beta
- gamma
loop_control:
extended: true
Available variables with extended: true:
| Variable | Description |
|---|---|
ansible_loop.index | Current iteration (1-based) |
ansible_loop.index0 | Current iteration (0-based) |
ansible_loop.first | True on first iteration |
ansible_loop.last | True on last iteration |
ansible_loop.length | Total number of items |
ansible_loop.revindex | Iterations remaining (1-based) |
ansible_loop.previtem | Previous item |
ansible_loop.nextitem | Next item |
Loop with Condition
# Only process items matching a condition
- name: Install production packages only
ansible.builtin.apt:
name: "{{ item.name }}"
loop:
- { name: nginx, envs: [production, staging] }
- { name: debug-tools, envs: [development] }
- { name: redis, envs: [production, staging, development] }
when: env in item.envs
# Skip certain items
- name: Create active users only
ansible.builtin.user:
name: "{{ item.name }}"
loop: "{{ users }}"
when: item.active | default(true)
Register with Loop
- name: Check service status
ansible.builtin.command: "systemctl is-active {{ item }}"
loop:
- nginx
- redis
- postgresql
register: service_status
ignore_errors: true
# Results are in service_status.results (list)
- name: Show failed services
ansible.builtin.debug:
msg: "{{ item.item }} is DOWN"
loop: "{{ service_status.results }}"
when: item.rc != 0
Nested Loops
# Loop over nested lists
- name: Create directories for each user
ansible.builtin.file:
path: "/home/{{ item.0 }}/{{ item.1 }}"
state: directory
owner: "{{ item.0 }}"
loop: "{{ users | product(directories) | list }}"
vars:
users: [alice, bob]
directories: [documents, projects, scripts]
# Creates: /home/alice/documents, /home/alice/projects, etc.
# With subelements
- name: Add SSH keys for each user
ansible.posix.authorized_key:
user: "{{ item.0.name }}"
key: "{{ item.1 }}"
loop: "{{ users | subelements('ssh_keys') }}"
vars:
users:
- name: alice
ssh_keys:
- "ssh-ed25519 AAAA... alice@laptop"
- "ssh-ed25519 BBBB... alice@desktop"
- name: bob
ssh_keys:
- "ssh-ed25519 CCCC... bob@laptop"
Loop with Fileglob
# Copy all config files
- name: Copy config files
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/myapp/
mode: "0644"
loop: "{{ lookup('fileglob', 'files/configs/*.conf') | split(',') }}"
# Template all .j2 files
- name: Deploy templates
ansible.builtin.template:
src: "{{ item }}"
dest: "/etc/myapp/{{ item | basename | regex_replace('\\.j2$', '') }}"
with_fileglob:
- templates/myapp/*.j2
Loop Control
# Custom loop variable name (for nested loops)
- name: Outer loop
ansible.builtin.include_tasks: inner.yml
loop:
- alice
- bob
loop_control:
loop_var: username # Use 'username' instead of 'item'
# Pause between iterations
- name: Rolling restart
ansible.builtin.service:
name: myapp
state: restarted
loop: "{{ groups['webservers'] }}"
loop_control:
pause: 30 # Wait 30 seconds between each host
# Custom label for output
- name: Deploy configs
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
loop:
- { src: app.conf.j2, dest: /etc/app.conf, env: prod }
- { src: db.conf.j2, dest: /etc/db.conf, env: prod }
loop_control:
label: "{{ item.dest }}" # Shows just the dest in output (not the whole dict)
until (Retry Loop)
# Retry until condition is met
- name: Wait for application to start
ansible.builtin.uri:
url: "http://localhost:8080/health"
status_code: 200
register: result
until: result.status == 200
retries: 10 # Max attempts
delay: 5 # Seconds between retries
# Retry command until output matches
- name: Wait for cluster to be ready
ansible.builtin.command: kubectl get nodes
register: nodes
until: "'Ready' in nodes.stdout"
retries: 30
delay: 10
Migration from with_* Keywords
| Old Syntax | Modern Syntax |
|---|---|
with_items: list | loop: list |
with_list: list | loop: list |
with_dict: dict | loop: "{{ dict | dict2items }}" |
with_fileglob: pattern | loop: "{{ lookup('fileglob', pattern, wantlist=True) }}" |
with_sequence: start=1 end=5 | loop: "{{ range(1, 6) | list }}" |
with_nested: [a, b] | loop: "{{ list_a | product(list_b) | list }}" |
with_subelements | loop: "{{ list | subelements('key') }}" |
Troubleshooting
"item is undefined"
You're using item outside a loop, or inside an include_tasks with a different loop_var:
# If outer loop uses loop_var: outer_item
# inner tasks must use outer_item, not item
Loop Over Empty List
# Avoid errors on empty lists
- name: Process items
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ my_list | default([]) }}"
Related Articles
Conclusion
loop is the modern way to iterate in Ansible. Use it with simple lists, dictionaries (dict2items), nested structures (product, subelements), and file patterns (fileglob). Key tips: use loop_control for custom variable names in nested loops, label to clean up output, and pause for rolling operations. Use until + retries for retry loops. For modules that accept lists natively (like apt), pass the whole list instead of looping.