Ansible Loops
Last updated: June 30, 2025
My Journey with Ansible Loops
When I first started working with Ansible, I quickly found myself repeating tasks in my playbooks. I was writing the same task multiple times with slight variations—creating users, installing packages, configuring services with different parameters. This approach not only made my playbooks unnecessarily lengthy but also difficult to maintain.
That's when I discovered the power of loops in Ansible. Learning to use loops transformed the way I write playbooks. Suddenly, I could define a single task that would execute multiple times with different inputs. My playbooks became shorter, more maintainable, and much more powerful.
In this blog post, I'll share my experience with Ansible loops, demonstrating how they can simplify automation across both Linux and Windows environments. Whether you're managing a handful of servers or a complex multi-platform infrastructure, mastering loops will take your Ansible automation to the next level.
Understanding Loops in Ansible
Loops in Ansible allow you to perform the same task multiple times with different values. This concept is similar to programming language loops, especially Python's for
loop. If you're familiar with other programming languages, you'll find Ansible loops quite intuitive.
Historically, Ansible provided various ways to implement loops:
The legacy
with_*
keywords (e.g.,with_items
,with_file
)The modern
loop
keyword (introduced in Ansible 2.5)
While with_*
keywords are still supported for backward compatibility, the Ansible documentation recommends using the loop
keyword for most use cases as it provides a more consistent and predictable interface.
Basic Loop Usage
Let's start with a simple example. Imagine you need to create multiple users on your Linux servers:
---
- name: Create multiple users
hosts: linux_servers
tasks:
- name: Add several users
ansible.builtin.user:
name: "{{ item }}"
state: present
groups: "wheel"
loop:
- devuser
- testuser
- adminuser
In this example, the user module will run three times, once for each value in the loop. The {{ item }}
variable holds the current value in each iteration.
For Windows servers, a similar approach can be used:
---
- name: Create multiple users
hosts: windows_servers
tasks:
- name: Add Windows users
win_user:
name: "{{ item }}"
password: "SecureP@ssw0rd"
state: present
groups: ["Users"]
loop:
- win_devuser
- win_testuser
- win_adminuser
Working with Lists of Dictionaries
Often, you'll need to provide multiple parameters for each iteration. For this, you can use a list of dictionaries:
Linux Example:
---
- name: Configure multiple users with specific settings
hosts: linux_servers
tasks:
- name: Add users with specific groups
ansible.builtin.user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
shell: "{{ item.shell | default('/bin/bash') }}"
loop:
- { name: 'devuser', groups: 'developers' }
- { name: 'testuser', groups: 'testers', shell: '/bin/zsh' }
- { name: 'adminuser', groups: 'sudo,admin' }
Windows Example:
---
- name: Configure multiple IIS websites
hosts: windows_servers
tasks:
- name: Create IIS websites
win_iis_website:
name: "{{ item.name }}"
state: started
port: "{{ item.port }}"
physical_path: "{{ item.path }}"
loop:
- { name: 'MainApp', port: 80, path: 'C:\\inetpub\\mainapp' }
- { name: 'API', port: 8080, path: 'C:\\inetpub\\api' }
- { name: 'Admin', port: 8090, path: 'C:\\inetpub\\admin' }
Iterating Over Dictionaries
Working with dictionaries in loops requires a different approach. Ansible provides the dict2items
filter for this purpose:
---
- name: Configure services with environment-specific settings
hosts: all
vars:
service_config:
web_server:
port: 80
max_connections: 200
app_server:
port: 8080
max_connections: 100
database:
port: 5432
max_connections: 50
tasks:
- name: Display service configurations
ansible.builtin.debug:
msg: "Service: {{ item.key }}, Port: {{ item.value.port }}, Max Connections: {{ item.value.max_connections }}"
loop: "{{ service_config | dict2items }}"
This task iterates through the dictionary, allowing you to access both the keys and values.
Nested Loops
For more complex scenarios, you might need nested loops. While Ansible doesn't have a direct nested loop construct, you can achieve the same result using Jinja2's product
filter:
---
- name: Configure database users across multiple databases
hosts: database_servers
tasks:
- name: Set up database access
ansible.builtin.mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
host: "%"
state: present
loop: "{{ ['app_user', 'read_user', 'admin_user'] | product(['customers', 'products', 'orders']) | list }}"
This will create all combinations of users and databases, resulting in nine iterations of the task.
Loop Control
Ansible provides the loop_control
directive to manage loop behavior:
Limiting Output
When dealing with large loops, the output can become unwieldy. Use the label
directive to make the output more readable:
---
- name: Install multiple packages
hosts: linux_servers
tasks:
- name: Install required packages
ansible.builtin.apt:
name: "{{ item.package }}"
state: present
loop:
- { package: 'nginx', version: 'latest' }
- { package: 'postgresql', version: '13' }
- { package: 'redis', version: '6.2' }
loop_control:
label: "{{ item.package }}"
Instead of showing the entire item dictionary in the output, Ansible will only display the package name.
Tracking Loop Progress
You can track the current position in a loop using the index_var
directive:
---
- name: Deploy application components in sequence
hosts: app_servers
tasks:
- name: Deploy components
ansible.builtin.debug:
msg: "Deploying component {{ item }} ({{ current_index + 1 }}/3)"
loop:
- database
- application
- frontend
loop_control:
index_var: current_index
Extended Loop Information
For advanced loop control, use the extended
option:
---
- name: Process items with advanced loop control
hosts: all
tasks:
- name: Process with extended information
ansible.builtin.debug:
msg: >
Processing {{ item }} -
First: {{ ansible_loop.first }},
Last: {{ ansible_loop.last }},
Index: {{ ansible_loop.index }}/{{ ansible_loop.length }}
loop:
- step1
- step2
- step3
- step4
loop_control:
extended: true
Looping with Conditionals
Combining loops with conditionals gives you even more flexibility:
Linux Example:
---
- name: Install packages based on distribution
hosts: linux_servers
tasks:
- name: Install packages
ansible.builtin.package:
name: "{{ item.package }}"
state: present
loop:
- { package: 'httpd', distro: 'CentOS' }
- { package: 'apache2', distro: 'Ubuntu' }
- { package: 'firewalld', distro: 'CentOS' }
- { package: 'ufw', distro: 'Ubuntu' }
when: ansible_facts['distribution'] == item.distro
Windows Example:
---
- name: Configure Windows features based on server role
hosts: windows_servers
tasks:
- name: Install Windows features
win_feature:
name: "{{ item.feature }}"
state: present
loop:
- { feature: 'Web-Server', role: 'web' }
- { feature: 'Web-Mgmt-Tools', role: 'web' }
- { feature: 'RSAT-AD-Tools', role: 'domain-controller' }
- { feature: 'RSAT-DNS-Server', role: 'domain-controller' }
when: server_role == item.role
Registering Variables with Loop
You can capture the results of a loop using the register
keyword:
---
- name: Check service status
hosts: all
tasks:
- name: Check if services are running
ansible.builtin.command: systemctl status {{ item }}
loop:
- nginx
- postgresql
- redis
register: service_status
changed_when: false
failed_when: false
- name: Display failed services
ansible.builtin.debug:
msg: "Service {{ item.item }} is not running"
loop: "{{ service_status.results }}"
when: item.rc != 0
When registering the result of a loop, Ansible creates a list in the .results
attribute of the registered variable. Each element in this list corresponds to an iteration of the loop.
Loop Until a Condition is Met
Sometimes you need to repeat a task until a specific condition is met. The until
keyword combined with retries
and delay
allows for this:
---
- name: Wait for service to be available
hosts: app_servers
tasks:
- name: Check if application is responding
ansible.builtin.uri:
url: "http://localhost:8080/health"
status_code: 200
register: result
until: result.status == 200
retries: 10
delay: 5
This task will retry up to 10 times with a 5-second delay between attempts until it gets a 200 status code from the health endpoint.
Sequence Diagram: How Loops Work in Ansible
Here's a sequence diagram that illustrates how loops are processed in Ansible:
This diagram shows how Ansible processes each item in a loop, replacing the item
variable with the current value before executing the task on the target host.
Performance Considerations
While loops are powerful, they can impact performance since each iteration requires a separate connection to the target host. Here are some tips for optimizing loop performance:
Use module parameters when possible: Many modules accept lists directly:
# Less efficient with loop - name: Install packages with loop ansible.builtin.apt: name: "{{ item }}" state: present loop: - nginx - postgresql - redis # More efficient direct approach - name: Install packages directly ansible.builtin.apt: name: - nginx - postgresql - redis state: present
Use
loop_control.pause
for rate limiting: When hitting external APIs or services that might have rate limits:- name: Make API calls with rate limiting ansible.builtin.uri: url: "https://api.example.com/resource/{{ item }}" method: GET loop: - resource1 - resource2 - resource3 loop_control: pause: 1 # 1 second pause between iterations
Real-World Examples
Let's look at some real-world examples that combine multiple concepts we've discussed.
Linux Server Configuration
---
- name: Configure multiple services on Linux servers
hosts: linux_servers
vars:
services:
- name: nginx
port: 80
config_template: "nginx.conf.j2"
state: started
- name: postgresql
port: 5432
config_template: "postgresql.conf.j2"
state: started
- name: redis
port: 6379
config_template: "redis.conf.j2"
state: started
tasks:
- name: Install services
ansible.builtin.package:
name: "{{ item.name }}"
state: present
loop: "{{ services }}"
- name: Configure service ports
ansible.builtin.template:
src: "{{ item.config_template }}"
dest: "/etc/{{ item.name }}/conf.d/custom.conf"
loop: "{{ services }}"
notify: "restart {{ item.name }}"
- name: Ensure services are running
ansible.builtin.service:
name: "{{ item.name }}"
state: "{{ item.state }}"
enabled: true
loop: "{{ services }}"
handlers:
- name: restart nginx
ansible.builtin.service:
name: nginx
state: restarted
- name: restart postgresql
ansible.builtin.service:
name: postgresql
state: restarted
- name: restart redis
ansible.builtin.service:
name: redis
state: restarted
Windows Environment Setup
---
- name: Configure Windows development environment
hosts: windows_dev_servers
vars:
dev_tools:
- name: "Visual Studio Code"
product_id: "{EA457B21-F73E-494C-ACAB-524FDE069978}"
download_url: "https://code.visualstudio.com/sha/download?build=stable&os=win32-x64"
- name: "Git for Windows"
product_id: "{1D1DB8ED-5D57-4CBC-B829-9B0EE5C508AD}"
download_url: "https://github.com/git-for-windows/git/releases/download/v2.37.1.windows.1/Git-2.37.1-64-bit.exe"
- name: "Node.js"
product_id: "{F4492219-6386-49A3-ADCE-04F8BEC74453}"
download_url: "https://nodejs.org/dist/v16.15.1/node-v16.15.1-x64.msi"
tasks:
- name: Download installation files
win_get_url:
url: "{{ item.download_url }}"
dest: "C:\\temp\\{{ item.name | replace(' ', '') }}-installer.exe"
loop: "{{ dev_tools }}"
- name: Install development tools
win_package:
path: "C:\\temp\\{{ item.name | replace(' ', '') }}-installer.exe"
product_id: "{{ item.product_id }}"
arguments: "/quiet /norestart"
state: present
loop: "{{ dev_tools }}"
- name: Create project directories
win_file:
path: "C:\\Projects\\{{ item }}"
state: directory
loop:
- Frontend
- Backend
- Database
- Documentation
Common Pitfalls and Solutions
Mistake: Using loop
with strings
loop
with strings# Incorrect
- name: Process string
ansible.builtin.debug:
msg: "{{ item }}"
loop: "hello" # This won't iterate over characters as expected
Solution: Convert the string to a list:
# Correct
- name: Process string characters
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ 'hello' | list }}" # Converts string to list of characters
Mistake: Nested structure flattening issues
# May not behave as expected
- name: Process nested list
ansible.builtin.debug:
msg: "{{ item }}"
loop:
- [1, 2]
- [3, 4]
Solution: Use the flatten
filter with appropriate level:
# Correct
- name: Process nested list
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ [[1, 2], [3, 4]] | flatten(1) }}"
Mistake: Not accessing loop results correctly
# Incorrect access
- name: Check files
ansible.builtin.stat:
path: "/tmp/{{ item }}"
loop:
- file1
- file2
register: file_stats
- name: Show results
ansible.builtin.debug:
msg: "{{ file_stats.stat.exists }}" # Incorrect, no 'stat' attribute in file_stats
Solution: Access through the results list:
# Correct access
- name: Show results
ansible.builtin.debug:
msg: "{{ item.stat.exists }}"
loop: "{{ file_stats.results }}"
Conclusion
Loops are a fundamental feature in Ansible that can significantly improve the efficiency and maintainability of your playbooks. They allow you to perform repetitive tasks with different inputs while keeping your code DRY (Don't Repeat Yourself). Whether you're managing Linux servers, Windows environments, or a hybrid infrastructure, mastering loops will help you create more elegant and powerful automation.
As I've discovered in my automation journey, the time invested in learning how to properly use loops in Ansible pays off immensely as your infrastructure grows and your playbooks become more complex. Start with simple loops and gradually incorporate the more advanced features as you gain confidence.
Last updated