Ansible OEL 8 DevOps · OEL 8 · Fundamentals

AnsibleLoops — loop, with_items, loop_control

The modern loop keyword, the legacy with_items family, looping over dicts, conditional loops, and how loop_control gives you per-iteration labels, pauses, and per-item index access.

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
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 }}
Sub-keyWhat it does
labelWhat to show in the run output (instead of dumping each whole dict)
loop_varRename item — useful inside nested includes
index_varVariable that exposes 0-based index of the current iteration
pauseSeconds to wait between iterations
extendedAdds 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.
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
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 }}}}"

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 }}}}"

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

Older playbooks use the with_* family. They still work but loop is the recommended replacement for almost all of them:

LegacyModern equivalent
with_items: listloop: list
with_dict: dloop: "{{ d | dict2items }}"
with_fileglob: "*.j2"loop: "{{ query('fileglob', '*.j2') }}"
with_subelementsloop: "{{ list | subelements('children') }}"
with_nestedloop: "{{ 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.