unit 5 · Logic
Loops, and the legacy with_*
One task, many items
you may want to read 12 · Playbook anatomy first — but nothing is locked
tl;dr
- loop: runs one task once per list element; the current element lands in the item variable.
- with_* is legacy, not deprecated — it still works and ansible-lint only warns. Write loop: in new code.
- loop_control fixes noisy output: label picks what each status line prints.
why it matters Real playbooks manage lists — packages, users, firewall rules — and every legacy repo is full of with_items you must read correctly before you can modernise it.
Lesson 12 had you writing one task per action. Now count the actions in a
real play: fifteen packages, six service accounts, a dozen firewall rules.
You are not writing thirty-three tasks. loop: is a task keyword that
takes a list and runs the task once per element. Inside each pass, the
current element is available as a variable named item:
- name: Install the baseline packages
ansible.builtin.dnf:
name: '{{ item }}' # the current element, each pass
state: present
loop: # a plain YAML list
- git
- htop
- tmuxThe run output shows one task header with three results under it, each
independently ok or changed. The module’s logic is untouched — dnf
still checks state before acting, so a second run reports ok three times.
Loops repeat tasks, not work. (One aside: package modules
accept a whole list in name: and resolve it in a single transaction,
which is faster than looping. The loop here shows the shape; you’ll reach
for it with modules that take one thing at a time.)
Lists of dicts
Real loops rarely run over bare strings. The workhorse pattern is a list of dictionaries, with dot access into the current one:
- name: Create the service accounts
ansible.builtin.user:
name: '{{ item.name }}' # dot access, like cfg_check.rc in lesson 15
groups: '{{ item.groups }}'
loop:
- { name: 'deploy', groups: 'docker' }
- { name: 'backup', groups: 'wheel' }
loop_control:
label: '{{ item.name }}' # status line shows this, not the whole dictWithout that loop_control: block, every per-item status line dumps the
entire dict. With two keys that’s noise; with a ten-key dict holding a
password hash, a real problem. label: chooses what prints instead. Two
siblings are worth knowing. index_var: names a zero-based counter for
the pass you’re on. loop_var: renames item itself — pointless now,
essential in lesson 25, where nested loops would otherwise both write to
item.
The with_* question
Open any Ansible repo more than a few years old and you won’t find loop:.
You’ll find this:
- name: Install the baseline packages # pre-2.5 style
ansible.builtin.dnf:
name: '{{ item }}'
state: present
with_items: [git, htop, tmux]with_items, with_dict, with_fileglob — a whole family, one keyword per
data shape. Ansible 2.5 introduced loop: to replace
the lot. One nuance to get exactly right, because half the
internet gets it wrong: blogs routinely call with_items deprecated.
Officially it is legacy — it still works, no removal has been announced,
and ansible-lint flags it as a warning, not an error. The team policy
that follows: write loop: in everything new, convert with_* when you’re
already editing a task, and don’t raise a PR that only churns loop syntax.
Two combinations round this out. Put lesson 18’s when: on a looping task
and it’s evaluated fresh for every element — when: item.create skips
individual items, not the whole task. register: on a looping task
stores a dict whose results key is a list, one result per pass. debug
that when a single item misbehaves. What loops can’t do yet is react:
change a config file for every item, and something still has to restart
the service exactly once at the end. That’s handlers, next lesson.
at work, this sounds like
- “Lint is flagging the with_items in that role.”
- ansible-lint warns on legacy with_* loops — the role still runs fine, but convert to loop: when you touch those tasks.
self-check — recall, not a test
1 A role you inherited uses with_items everywhere. Is that broken, deprecated, or something else — and what does ansible-lint say about it?
2 You swap with_items for loop and a task that was fed a list of lists breaks. Why, and what is the fix?
3 Your loop over dicts prints an unreadable wall of keys per item. Which keyword cleans that up?
lab (optional — never required)
Loop ansible.builtin.user (or debug on localhost) over a list of three dicts, watch the noisy output, then add loop_control: label and rerun.
needs: The control node from lesson 5.