Modern Loop Syntax
# The modern way (Ansible 2.5+)
- name: Install packages
ansible.builtin.apt:
name: "{{ item }}"
state: present
loop:
- nginx
- git
- curl
# Even better — pass list directly
- name: Install packages
ansible.builtin.apt:
name:
- nginx
- git
- curl
state: present
Migration from with_items
# OLD — with_items (still works but deprecated style)
- name: Create users
ansible.builtin.user:
name: "{{ item }}"
with_items:
- alice
- bob
# NEW — loop (preferred)
- name: Create users
ansible.builtin.user:
name: "{{ item }}"
loop:
- alice
- bob
Loop with Dictionaries
# Loop over list of dicts
- name: Create users with groups
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop:
- { name: alice, groups: admin }
- { name: bob, groups: developers }
# OLD with_dict → NEW loop + dict2items
- name: Set sysctl values
ansible.posix.sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
loop: "{{ sysctl_settings | dict2items }}"
vars:
sysctl_settings:
net.ipv4.ip_forward: 1
net.ipv4.conf.all.forwarding: 1
Loop Control
# Custom loop variable (avoids conflicts in nested loops)
- name: Outer loop
ansible.builtin.include_tasks: inner.yml
loop:
- server1
- server2
loop_control:
loop_var: outer_item
# Show label instead of full item
- name: Deploy apps
ansible.builtin.command: deploy {{ item.name }}
loop: "{{ apps }}"
loop_control:
label: "{{ item.name }}" # Shows just the name, not full dict
# Access loop index
- name: Numbered output
ansible.builtin.debug:
msg: "{{ index }}: {{ item }}"
loop:
- first
- second
- third
loop_control:
index_var: index
Conditional Loops
- name: Start only enabled services
ansible.builtin.service:
name: "{{ item.name }}"
state: started
loop: "{{ services }}"
when: item.enabled | bool
vars:
services:
- { name: nginx, enabled: true }
- { name: postgres, enabled: false }
Loop with Register
- name: Check services
ansible.builtin.command: systemctl status {{ item }}
loop:
- nginx
- postgresql
- redis
register: service_status
ignore_errors: true
- name: Show failed services
ansible.builtin.debug:
msg: "{{ item.item }} is down!"
loop: "{{ service_status.results }}"
when: item.rc != 0
Flatten & Subelements
# Flatten nested lists
- name: Install all packages
ansible.builtin.apt:
name: "{{ item }}"
loop: "{{ packages | flatten }}"
vars:
packages:
- [nginx, git]
- [curl, wget]
# Subelements — loop over nested structure
- name: Add SSH keys for users
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
Performance: until + retries
- name: Wait for service to be ready
ansible.builtin.uri:
url: http://localhost:8080/health
register: health
until: health.status == 200
retries: 30
delay: 5
Conclusion
Use loop for all new playbooks. Migrate with_* gradually. Use loop_control for labels and nested loop variables. Pass lists directly to modules when possible — it's faster than looping.