AnsibleXV · Conditionals and LoopsLoops
loop_control and readable runs
What you'll learn
- Use label to keep a loop over structured data out of the run log
- Rename the loop variable to avoid a collision in nested loops
- Distinguish index_var from the extended ansible_loop index
- Judge when pause is a legitimate control and when serial is the right tool
Prerequisites
Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11
loop_control looks like a set of cosmetic options for tidying up run
output. One of them is a disclosure control, and it defaults to the
unsafe setting.
label: what the run log prints
By default, Ansible prints the whole item on every iteration. For a
list of strings that is fine. For a list of dictionaries it is not:
$ ansible-playbook lc.ymlTASK [Without label the whole dict prints] *************************************
ok: [localhost] => (item={'name': 'svc_backup', 'token': 'REPLACE_ME_1'}) => {
"msg": "configuring svc_backup"
}
ok: [localhost] => (item={'name': 'svc_deploy', 'token': 'REPLACE_ME_2'}) => {
"msg": "configuring svc_deploy"
}The task never referenced item.token. It printed anyway, because the
default label is the whole item and the whole item includes every key.
label replaces what is printed:
$ ansible-playbook lc.ymlTASK [With label only the name prints] *****************************************
ok: [localhost] => (item=svc_backup) => {
"msg": "configuring svc_backup"
}
ok: [localhost] => (item=svc_deploy) => {
"msg": "configuring svc_deploy"
}- name: Configure the service accounts
ansible.builtin.debug:
msg: "configuring {{ item.name }}"
loop: "{{ accounts }}"
loop_control:
label: "{{ item.name }}"loop_var: avoiding a collision
item is a variable like any other, at task-var precedence. Nested
loops therefore collide: an outer loop binds item, an inner one binds
item, and the inner wins.
The classic case is a task file included in a loop, which itself contains a loop:
# site.yml
- name: Configure each site
ansible.builtin.include_tasks: configure_site.yml
loop: "{{ sites }}"
loop_control:
loop_var: site
label: "{{ site.name }}"
# configure_site.yml - free to use item for its own loop
- name: Deploy each vhost file for this site
ansible.builtin.template:
src: "{{ item }}.j2"
dest: "/etc/nginx/sites-available/{{ item }}"
mode: '0644'
loop: "{{ site.vhosts }}"The rule worth adopting: any loop on include_tasks or include_role
renames its loop variable. You cannot know what the included file
loops over, and the collision is silent — the inner value simply wins
and the outer task appears to process the wrong data.
Renaming also improves readability at no cost. site.name says what it
is; item.name requires the reader to scroll up.
index_var and extended
Two ways to know where you are in a loop, and they count differently.
$ ansible-playbook lc.ymlTASK [index_var and a renamed loop_var] ****************************************
ok: [localhost] => (item=svc_backup) => {
"msg": "index 0 -> svc_backup"
}
ok: [localhost] => (item=svc_deploy) => {
"msg": "index 1 -> svc_deploy"
}$ ansible-playbook lc.ymlTASK [extended gives ansible_loop] *********************************************
ok: [localhost] => (item=svc_backup) => {
"msg": "1/2 first=True last=False revindex=2"
}
ok: [localhost] => (item=svc_deploy) => {
"msg": "2/2 first=False last=True revindex=1"
}index_var is zero-based. ansible_loop.index is one-based. Both
appear in the same output above — index 0 and index 1 from index_var,
1/2 and 2/2 from ansible_loop. Mixing them is a classic off-by-one.
extended provides index, index0, revindex, revindex0,
first, last, length, previtem, nextitem and allitems. The
useful ones in practice:
| Key | Use |
|---|---|
ansible_loop.first | Do something only on the first item |
ansible_loop.last | Flush, reload or report only at the end |
ansible_loop.length | Include a progress count in a message |
ansible_loop.allitems | The whole list, from inside an iteration |
allitems carries a memory cost, because every iteration’s result
holds a copy of the entire list. extended_allitems: false turns just
that off while keeping the rest of extended — worth setting for a
loop over anything large.
pause: a rate limit, not a rollout control
pause inserts a delay in seconds between iterations.
- name: Register each host with the inventory API
ansible.builtin.uri:
url: "https://cmdb.example.com/api/v1/hosts"
method: POST
body_format: json
body:
hostname: "{{ item.name }}"
headers:
Authorization: "Bearer {{ cmdb_token }}"
status_code: [200, 201]
loop: "{{ host_records }}"
loop_control:
label: "{{ item.name }}"
pause: 2
no_log: trueThat is what pause is for: an external service with a rate limit, or
a device that needs settling time between operations.
The other reason to be wary: pause is dead time inside the run.
pause: 2 over 50 items is 100 seconds per host, serialised, during
which the connection is held open and the play cannot proceed. On a
large fleet that is a real cost, and it is worth asking whether the
rate limit could be handled by batching the request instead.
A default worth adopting
- name: Deploy each application config
ansible.builtin.template:
src: "{{ app.template }}"
dest: "/etc/{{ app.name }}/config.yml"
mode: '0640'
loop: "{{ applications }}"
loop_control:
loop_var: app
label: "{{ app.name }}"Two lines of loop_control on every loop over structured data. It
costs nothing, it keeps the run log readable, and it means the day
somebody adds a secret to that structure the run log does not
immediately publish it.
Knowledge check
Knowledge check · 4 questions
Q1. A task loops over a list of dictionaries, each containing name and token. The task only ever references item.name. What appears in the run log by default?
Q2. A play needs to restart a service across forty hosts without taking them all down at once. Is loop_control.pause the right tool?
Q3. Which statements about loop_control are accurate? Select all that apply.
Q4. A label expression that references a key missing from one item in the list will fail the task, even though the module itself would have succeeded.
Passing score: 75%. Answers are checked in this browser.