AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible Loops: Complete Guide with loop, with_items & Examples

By Luca Berton · Published 2026-04-03 · Category: installation

Complete guide to Ansible loops. Use loop, with_items, with_dict, loop_control, and complex iteration patterns with practical examples.

Loops in Ansible let you repeat a task for multiple items — packages to install, users to create, files to deploy. The modern loop keyword replaces the older with_ syntax.

Basic loop

- name: Install multiple packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - postgresql
    - redis-server
    - python3-pip

See also: Ansible when Conditional: Complete Guide with Examples

loop with Dictionaries

- name: Create users with specific UIDs
  ansible.builtin.user:
    name: "{{ item.name }}"
    uid: "{{ item.uid }}"
    groups: "{{ item.groups }}"
    state: present
  loop:
    - { name: alice, uid: 1001, groups: admin }
    - { name: bob, uid: 1002, groups: developers }
    - { name: charlie, uid: 1003, groups: developers }

loop with Variables

vars:
  packages:
    - nginx
    - postgresql
    - redis

tasks: - name: Install packages from variable ansible.builtin.apt: name: "{{ item }}" state: present loop: "{{ packages }}"

See also: Ansible with_items vs loop: Migration Guide & Key Differences (2026)

with_items (Legacy, Still Works)

- name: Install packages (legacy syntax)
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  with_items:
    - nginx
    - postgresql

Migration: with_itemsloop (direct replacement for simple lists)

with_dict / loop + dict2items

# Legacy
- name: Set sysctl values
  ansible.posix.sysctl:
    name: "{{ item.key }}"
    value: "{{ item.value }}"
  with_dict:
    net.ipv4.ip_forward: 1
    net.ipv4.conf.all.forwarding: 1

# Modern - name: Set sysctl values (modern) ansible.posix.sysctl: name: "{{ item.key }}" value: "{{ item.value }}" loop: "{{ sysctl_params | dict2items }}" vars: sysctl_params: net.ipv4.ip_forward: 1 net.ipv4.conf.all.forwarding: 1

See also: Ansible check_mode: Dry Run & Test Playbooks Without Making Changes

with_fileglob / loop + fileglob

# Copy all config files
- name: Copy configs
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: /etc/myapp/conf.d/
  loop: "{{ lookup('fileglob', 'files/configs/*.conf', wantlist=True) }}"

Nested Loops (with_nested / product)

# Create user directories for each environment
- name: Create user env directories
  ansible.builtin.file:
    path: "/opt/{{ item.0 }}/{{ item.1 }}"
    state: directory
  loop: "{{ ['dev', 'staging', 'prod'] | product(['logs', 'data', 'config']) | list }}"

loop with Filters

Select specific items

- name: Install only required packages
  ansible.builtin.apt:
    name: "{{ item.name }}"
    state: present
  loop: "{{ packages }}"
  when: item.required | default(true)

Unique items

- name: Process unique values only
  ansible.builtin.debug:
    msg: "{{ item }}"
  loop: "{{ my_list | unique }}"

Flatten nested lists

- name: Install all packages from nested lists
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ [base_packages, app_packages, monitoring_packages] | flatten }}"

loop with Register

- name: Check service status
  ansible.builtin.command: "systemctl is-active {{ item }}"
  loop:
    - nginx
    - postgresql
    - redis
  register: service_checks
  ignore_errors: true

- name: Show failed services ansible.builtin.debug: msg: "{{ item.item }} is not running" loop: "{{ service_checks.results }}" when: item.rc != 0

loop with index (loop_control)

- name: Create numbered workers
  ansible.builtin.template:
    src: worker.conf.j2
    dest: "/etc/workers/worker-{{ idx }}.conf"
  loop:
    - { port: 8001, workers: 4 }
    - { port: 8002, workers: 2 }
    - { port: 8003, workers: 2 }
  loop_control:
    index_var: idx
    label: "worker-{{ idx }}"

until: Retry Loops

- name: Wait for service to be ready
  ansible.builtin.uri:
    url: http://localhost:8080/health
    status_code: 200
  register: health
  until: health.status == 200
  retries: 30
  delay: 10

Efficient Package Installation

# DON'T: Loop over apt (slow - runs apt for each)
- name: Bad - one apt call per package
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: "{{ packages }}"

# DO: Pass list directly (one apt call) - name: Good - all packages at once ansible.builtin.apt: name: "{{ packages }}" state: present

Migration Reference: with_ → loop

| Old Syntax | New Syntax | |------------|-----------| | with_items: list | loop: list | | with_list: list | loop: list | | with_dict: dict | loop: "{{ dict \| dict2items }}" | | with_fileglob: pattern | loop: "{{ lookup('fileglob', pattern, wantlist=True) }}" | | with_sequence: ... | loop: "{{ range(1, 11) \| list }}" | | with_nested: [a, b] | loop: "{{ a \| product(b) \| list }}" | | with_subelements | loop: "{{ list \| subelements('key') }}" |

FAQ

What's the difference between loop and with_items?

Functionally identical for simple lists. loop is the modern syntax (Ansible 2.5+). with_items flattens nested lists; loop does not — use loop: "{{ list | flatten }}" if needed.

How do I loop over a range of numbers?

loop: "{{ range(1, 11) | list }}" creates [1, 2, 3, ..., 10].

Can I break out of a loop?

Ansible doesn't support break. Use when to skip items, or restructure your task to process only needed items.

Basic Loop

- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - curl
    - htop
    - vim
  become: true

Loop Over List of Dicts

- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    shell: "{{ item.shell | default('/bin/bash') }}"
  loop:
    - { name: alice, groups: "sudo,docker" }
    - { name: bob, groups: "docker" }
    - { name: charlie, groups: "www-data" }
  become: true

Loop Control

- name: Process servers
  debug:
    msg: "{{ idx }}: {{ item.name }}"
  loop: "{{ servers }}"
  loop_control:
    index_var: idx          # Loop index
    label: "{{ item.name }}" # Clean output
    pause: 2                # Seconds between iterations
    extended: true          # Access ansible_loop.*

Until (Retry Loop)

- name: Wait for service
  ansible.builtin.uri:
    url: http://localhost:8080/health
    status_code: 200
  register: result
  until: result.status == 200
  retries: 30
  delay: 10  # seconds between retries

Loop Over Dictionary

- vars:
    users:
      alice: admin
      bob: developer
      charlie: readonly
  debug:
    msg: "{{ item.key }} is {{ item.value }}"
  loop: "{{ users | dict2items }}"

Nested Loops

- name: Grant database permissions
  command: >
    mysql -e "GRANT ALL ON {{ item[0] }}.* TO '{{ item[1] }}'@'localhost'"
  loop: "{{ databases | product(db_users) | list }}"
  vars:
    databases: [app_db, analytics_db]
    db_users: [appuser, readonly]

Loop Over Files

- name: Deploy config files
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: /etc/myapp/
  loop: "{{ lookup('fileglob', 'files/configs/*.conf', wantlist=True) }}"

Register with Loops

- command: "systemctl is-active {{ item }}"
  loop: [nginx, redis, postgresql]
  register: services
  ignore_errors: true
  changed_when: false

- debug: msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}" loop: "{{ services.results }}"

Flatten Nested Lists

- vars:
    packages:
      - [nginx, curl]
      - [vim, htop]
      - git
  apt:
    name: "{{ item }}"
  loop: "{{ packages | flatten }}"

Loop vs with_

| Modern (loop) | Legacy (with_) | |---------------|----------------| | loop: list | with_items: list | | loop: "{{ dict \| dict2items }}" | with_dict: dict | | loop: "{{ a \| product(b) }}" | with_nested: [a, b] | | loop: "{{ lookup('fileglob', '.conf', wantlist=True) }}" | with_fileglob: ".conf" | | loop: "{{ range(1, 11) }}" | with_sequence: start=1 end=10 |

FAQ

loop vs with_items?

loop is the modern syntax (Ansible 2.5+). with_items still works but loop is recommended for new playbooks.

How do I break out of a loop?

Ansible doesn't support breaking. Filter the list before looping: loop: "{{ items | selectattr('active') | list }}".

Can I loop over inventory hosts?

loop: "{{ groups['webservers'] }}"

Basic Loop

- apt:
    name: "{{ item }}"
    state: present
  loop:
    - nginx
    - redis-server
    - postgresql
  become: true

Loop Over Dictionary

- user:
    name: "{{ item.key }}"
    groups: "{{ item.value.groups }}"
    shell: "{{ item.value.shell }}"
  loop: "{{ users | dict2items }}"
  vars:
    users:
      alice: { groups: [admin, docker], shell: /bin/bash }
      bob: { groups: [developers], shell: /bin/zsh }

Loop with Index

- debug:
    msg: "{{ index }}: {{ item }}"
  loop: "{{ packages }}"
  loop_control:
    index_var: index

Nested Loops (subelements)

- user:
    name: "{{ item.0.name }}"
    groups: "{{ item.1 }}"
    append: true
  loop: "{{ users | subelements('groups') }}"
  vars:
    users:
      - { name: alice, groups: [admin, docker] }
      - { name: bob, groups: [developers, docker] }

Loop with Conditionals

- apt:
    name: "{{ item.name }}"
  loop:
    - { name: nginx, install: true }
    - { name: apache2, install: false }
    - { name: redis, install: true }
  when: item.install

Loop Control

- debug:
    msg: "Installing {{ pkg }}"
  loop: "{{ packages }}"
  loop_control:
    loop_var: pkg        # Rename 'item'
    label: "{{ pkg }}"   # Display label in output
    pause: 2             # Seconds between iterations
    index_var: idx       # Index variable

Loop Over Files

- copy:
    src: "{{ item }}"
    dest: /etc/myapp/
  loop: "{{ lookup('fileglob', 'configs/*.conf') | list }}"

Until Loop (Retry)

- uri:
    url: http://localhost:8080/health
  register: result
  until: result.status == 200
  retries: 10
  delay: 5

Flatten Nested Lists

- debug:
    msg: "{{ item }}"
  loop: "{{ nested_list | flatten }}"
  vars:
    nested_list:
      - [nginx, redis]
      - [postgresql]
      - [docker, podman]

Register with Loops

- command: "systemctl status {{ item }}"
  loop: [nginx, redis, postgresql]
  register: service_status
  ignore_errors: true

- debug: msg: "{{ item.item }}: {{ 'running' if item.rc == 0 else 'stopped' }}" loop: "{{ service_status.results }}"

FAQ

loop vs with_items?

loop is the modern syntax (Ansible 2.5+). with_items still works but loop is recommended. They're functionally equivalent for simple lists.

How to break out of a loop?

Ansible doesn't have break. Use when to skip iterations, or restructure as until for retry-style loops.

Performance: loop vs package list?

For package managers, passing a list is faster:

# Faster (single transaction)
apt: { name: [nginx, redis, postgresql] }
# Slower (one transaction per item)
loop: [nginx, redis, postgresql]

Related Articles

using ansible.builtin.template effectivelyAnsible loop_control Guidewhen expressions and Jinja2 in AnsibleAnsible Ignore Errors GuideNginx vhost provisioning with Ansible

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home