Ansible Loops: Complete Guide with loop, with_items & Examples
By Luca Berton · Published 2026-04-03 · Category: installation
Complete guide to Ansible loops. Use loop, with_items, with_dict, loop_control, and complex iteration patterns with practical examples.
Loops in Ansible let you repeat a task for multiple items — packages to install, users to create, files to deploy. The modern loop keyword replaces the older with_ syntax.
Basic loop
- name: Install multiple packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis-server
- python3-pip
See also: Ansible when Conditional: Complete Guide with Examples
loop with Dictionaries
- name: Create users with specific UIDs
ansible.builtin.user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: alice, uid: 1001, groups: admin }
- { name: bob, uid: 1002, groups: developers }
- { name: charlie, uid: 1003, groups: developers }
loop with Variables
vars:
packages:
- nginx
- postgresql
- redis
tasks:
- name: Install packages from variable
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
See also: Ansible with_items vs loop: Migration Guide & Key Differences (2026)
with_items (Legacy, Still Works)
- name: Install packages (legacy syntax)
ansible.builtin.apt:
name: "{{ item }}"
state: present
with_items:
- nginx
- postgresql
Migration: with_items → loop (direct replacement for simple lists)
with_dict / loop + dict2items
# Legacy
- name: Set sysctl values
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
with_dict:
net.ipv4.ip_forward: 1
net.ipv4.conf.all.forwarding: 1
# Modern
- name: Set sysctl values (modern)
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
loop: "{{ sysctl_params | dict2items }}"
vars:
sysctl_params:
net.ipv4.ip_forward: 1
net.ipv4.conf.all.forwarding: 1
See also: Ansible check_mode: Dry Run & Test Playbooks Without Making Changes
with_fileglob / loop + fileglob
# Copy all config files
- name: Copy configs
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/myapp/conf.d/
loop: "{{ lookup('fileglob', 'files/configs/*.conf', wantlist=True) }}"
Nested Loops (with_nested / product)
# Create user directories for each environment
- name: Create user env directories
ansible.builtin.file:
path: "/opt/{{ item.0 }}/{{ item.1 }}"
state: directory
loop: "{{ ['dev', 'staging', 'prod'] | product(['logs', 'data', 'config']) | list }}"
loop with Filters
Select specific items
- name: Install only required packages
ansible.builtin.apt:
name: "{{ item.name }}"
state: present
loop: "{{ packages }}"
when: item.required | default(true)
Unique items
- name: Process unique values only
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ my_list | unique }}"
Flatten nested lists
- name: Install all packages from nested lists
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ [base_packages, app_packages, monitoring_packages] | flatten }}"
loop with Register
- name: Check service status
ansible.builtin.command: "systemctl is-active {{ item }}"
loop:
- nginx
- postgresql
- redis
register: service_checks
ignore_errors: true
- name: Show failed services
ansible.builtin.debug:
msg: "{{ item.item }} is not running"
loop: "{{ service_checks.results }}"
when: item.rc != 0
loop with index (loop_control)
- name: Create numbered workers
ansible.builtin.template:
src: worker.conf.j2
dest: "/etc/workers/worker-{{ idx }}.conf"
loop:
- { port: 8001, workers: 4 }
- { port: 8002, workers: 2 }
- { port: 8003, workers: 2 }
loop_control:
index_var: idx
label: "worker-{{ idx }}"
until: Retry Loops
- name: Wait for service to be ready
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: health
until: health.status == 200
retries: 30
delay: 10
Efficient Package Installation
# DON'T: Loop over apt (slow - runs apt for each)
- name: Bad - one apt call per package
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop: "{{ packages }}"
# DO: Pass list directly (one apt call)
- name: Good - all packages at once
ansible.builtin.apt:
name: "{{ packages }}"
state: present
Migration Reference: with_ → loop
| Old Syntax | New 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: ... | loop: "{{ range(1, 11) \| list }}" |
| with_nested: [a, b] | loop: "{{ a \| product(b) \| list }}" |
| with_subelements | loop: "{{ list \| subelements('key') }}" |
FAQ
What's the difference between loop and with_items?
Functionally identical for simple lists. loop is the modern syntax (Ansible 2.5+). with_items flattens nested lists; loop does not — use loop: "{{ list | flatten }}" if needed.
How do I loop over a range of numbers?
loop: "{{ range(1, 11) | list }}" creates [1, 2, 3, ..., 10].
Can I break out of a loop?
Ansible doesn't support break. Use when to skip items, or restructure your task to process only needed items.
Basic Loop
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- curl
- htop
- vim
become: true
Loop Over 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: "sudo,docker" }
- { name: bob, groups: "docker" }
- { name: charlie, groups: "www-data" }
become: true
Loop Control
- name: Process servers
debug:
msg: "{{ idx }}: {{ item.name }}"
loop: "{{ servers }}"
loop_control:
index_var: idx # Loop index
label: "{{ item.name }}" # Clean output
pause: 2 # Seconds between iterations
extended: true # Access ansible_loop.*
Until (Retry Loop)
- name: Wait for service
ansible.builtin.uri:
url: http://localhost:8080/health
status_code: 200
register: result
until: result.status == 200
retries: 30
delay: 10 # seconds between retries
Loop Over Dictionary
- vars:
users:
alice: admin
bob: developer
charlie: readonly
debug:
msg: "{{ item.key }} is {{ item.value }}"
loop: "{{ users | dict2items }}"
Nested Loops
- name: Grant database permissions
command: >
mysql -e "GRANT ALL ON {{ item[0] }}.* TO '{{ item[1] }}'@'localhost'"
loop: "{{ databases | product(db_users) | list }}"
vars:
databases: [app_db, analytics_db]
db_users: [appuser, readonly]
Loop Over Files
- name: Deploy config files
ansible.builtin.copy:
src: "{{ item }}"
dest: /etc/myapp/
loop: "{{ lookup('fileglob', 'files/configs/*.conf', wantlist=True) }}"
Register with Loops
- command: "systemctl is-active {{ item }}"
loop: [nginx, redis, postgresql]
register: services
ignore_errors: true
changed_when: false
- debug:
msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}"
loop: "{{ services.results }}"
Flatten Nested Lists
- vars:
packages:
- [nginx, curl]
- [vim, htop]
- git
apt:
name: "{{ item }}"
loop: "{{ packages | flatten }}"
Loop vs with_
| Modern (loop) | Legacy (with_) |
|---------------|----------------|
| loop: list | with_items: list |
| loop: "{{ dict \| dict2items }}" | with_dict: dict |
| loop: "{{ a \| product(b) }}" | with_nested: [a, b] |
| loop: "{{ lookup('fileglob', '.conf', wantlist=True) }}" | with_fileglob: ".conf" |
| loop: "{{ range(1, 11) }}" | with_sequence: start=1 end=10 |
FAQ
loop vs with_items?
loop is the modern syntax (Ansible 2.5+). with_items still works but loop is recommended for new playbooks.
How do I break out of a loop?
Ansible doesn't support breaking. Filter the list before looping: loop: "{{ items | selectattr('active') | list }}".
Can I loop over inventory hosts?
loop: "{{ groups['webservers'] }}"
Basic Loop
- apt:
name: "{{ item }}"
state: present
loop:
- nginx
- redis-server
- postgresql
become: true
Loop Over Dictionary
- user:
name: "{{ item.key }}"
groups: "{{ item.value.groups }}"
shell: "{{ item.value.shell }}"
loop: "{{ users | dict2items }}"
vars:
users:
alice: { groups: [admin, docker], shell: /bin/bash }
bob: { groups: [developers], shell: /bin/zsh }
Loop with Index
- debug:
msg: "{{ index }}: {{ item }}"
loop: "{{ packages }}"
loop_control:
index_var: index
Nested Loops (subelements)
- user:
name: "{{ item.0.name }}"
groups: "{{ item.1 }}"
append: true
loop: "{{ users | subelements('groups') }}"
vars:
users:
- { name: alice, groups: [admin, docker] }
- { name: bob, groups: [developers, docker] }
Loop with Conditionals
- apt:
name: "{{ item.name }}"
loop:
- { name: nginx, install: true }
- { name: apache2, install: false }
- { name: redis, install: true }
when: item.install
Loop Control
- debug:
msg: "Installing {{ pkg }}"
loop: "{{ packages }}"
loop_control:
loop_var: pkg # Rename 'item'
label: "{{ pkg }}" # Display label in output
pause: 2 # Seconds between iterations
index_var: idx # Index variable
Loop Over Files
- copy:
src: "{{ item }}"
dest: /etc/myapp/
loop: "{{ lookup('fileglob', 'configs/*.conf') | list }}"
Until Loop (Retry)
- uri:
url: http://localhost:8080/health
register: result
until: result.status == 200
retries: 10
delay: 5
Flatten Nested Lists
- debug:
msg: "{{ item }}"
loop: "{{ nested_list | flatten }}"
vars:
nested_list:
- [nginx, redis]
- [postgresql]
- [docker, podman]
Register with Loops
- command: "systemctl status {{ item }}"
loop: [nginx, redis, postgresql]
register: service_status
ignore_errors: true
- debug:
msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}"
loop: "{{ service_status.results }}"
FAQ
loop vs with_items?
loop is the modern syntax (Ansible 2.5+). with_items still works but loop is recommended. They're functionally equivalent for simple lists.
How to break out of a loop?
Ansible doesn't have break. Use when to skip iterations, or restructure as until for retry-style loops.
Performance: loop vs package list?
For package managers, passing a list is faster:
# Faster (single transaction)
apt: { name: [nginx, redis, postgresql] }
# Slower (one transaction per item)
loop: [nginx, redis, postgresql]
Related Articles
• using ansible.builtin.template effectively • Ansible loop_control Guide • when expressions and Jinja2 in Ansible • Ansible Ignore Errors Guide • Nginx vhost provisioning with AnsibleCategory: installation