loop — The Modern Way
loop is the recommended way to repeat a task. It accepts a list and runs the task
once per item with the variable item in scope.
YAML — basic loop
- name: Install database tools
ansible.builtin.dnf:
name: "{{{{ item }}}}"
state: present
loop:
- mariadb
- percona-toolkit
- mysqltuner
💡 Tip: If you can install multiple packages in one task call (
name: accepts a list), do that instead of looping. It's faster — one task per host instead of one per item per host.YAML — list-arg without loop (faster)
- name: Install database tools (single task)
ansible.builtin.dnf:
name:
- mariadb
- percona-toolkit
- mysqltuner
state: present
Looping Over Lists of Dicts
YAML — loop over user definitions
- name: Create application users
ansible.builtin.user:
name: "{{{{ item.name }}}}"
uid: "{{{{ item.uid }}}}"
shell: "{{{{ item.shell | default('/bin/bash') }}}}"
state: present
loop:
- {{ name: alice, uid: 1001, shell: /bin/zsh }}
- {{ name: bob, uid: 1002 }}
- {{ name: carol, uid: 1003, shell: /bin/fish }}
loop_control — Customise the Loop
| Sub-key | What it does |
|---|---|
label | What to show in the run output (instead of dumping each whole dict) |
loop_var | Rename item — useful inside nested includes |
index_var | Variable that exposes 0-based index of the current iteration |
pause | Seconds to wait between iterations |
extended | Adds ansible_loop.first/.last/.length/.index to the loop scope |
YAML — loop_control in action
- name: Create users with clean output and per-item label
ansible.builtin.user:
name: "{{{{ item.name }}}}"
uid: "{{{{ item.uid }}}}"
state: present
loop: "{{{{ users }}}}"
loop_control:
label: "{{{{ item.name }}}} (uid={{{{ item.uid }}}})"
index_var: my_idx
# Output reads:
# changed: [host1] => (item=alice (uid=1001))
# changed: [host1] => (item=bob (uid=1002))
# Instead of dumping the whole dict on each iteration.
Looping Over a Range of Numbers
YAML — range loop
- name: Create 5 numbered db schemas
community.mysql.mysql_db:
name: "shard_{{{{ '%02d' | format(item) }}}}"
state: present
loop: "{{{{ range(1, 6) | list }}}}"
# creates shard_01, shard_02, ..., shard_05
Looping Over a Dict
YAML — dict2items
- hosts: localhost
vars:
services:
mysqld: 3306
nginx: 80
redis: 6379
tasks:
- name: Open service ports
ansible.posix.firewalld:
port: "{{{{ item.value }}}}/tcp"
state: enabled
permanent: true
loop: "{{{{ services | dict2items }}}}"
loop_control:
label: "{{{{ item.key }}}} → port {{{{ item.value }}}}"
Conditional Loops
when evaluates per iteration — combine it with loop to filter:
YAML — filter inside a loop
- name: Only enable production users
ansible.builtin.user:
name: "{{{{ item.name }}}}"
state: present
loop: "{{{{ users }}}}"
when: item.env == "production"
loop_control:
label: "{{{{ item.name }}}}"
Pausing Between Iterations
For rolling updates where you want some time between hosts (or some time between items),
combine pause with loop_control:
YAML — paced loop
- name: Restart services with 10s gap
ansible.builtin.systemd:
name: "{{{{ item }}}}"
state: restarted
loop:
- mysqld
- mysql-router
- haproxy
loop_control:
pause: 10
with_items, with_dict, with_fileglob — Legacy
Older playbooks use the with_* family. They still work but loop is the
recommended replacement for almost all of them:
| Legacy | Modern equivalent |
|---|---|
with_items: list | loop: list |
with_dict: d | loop: "{{ d | dict2items }}" |
with_fileglob: "*.j2" | loop: "{{ query('fileglob', '*.j2') }}" |
with_subelements | loop: "{{ list | subelements('children') }}" |
with_nested | loop: "{{ a | product(b) | list }}" |
✅ Tip: Mixed with_* and loop within the same role is fine — there's no migration urgency. But for new code, default to
loop: future Ansible versions will keep loop as the preferred style.