
Using Loops in Ansible Playbooks
Loops are one of Ansible’s most powerful features, allowing you to iterate over lists, dictionaries, and other data structures to execute tasks multiple times with different parameters. Instead of writing repetitive tasks for similar operations, loops help you create cleaner, more maintainable playbooks while reducing code duplication. This guide will walk you through the various loop types available in Ansible, practical implementation examples, common troubleshooting scenarios, and best practices that will make your automation workflows more efficient.
How Ansible Loops Work
Ansible loops operate by taking a list of items and executing a task once for each item in that list. The current item is accessible through the item
variable within the task. Modern Ansible versions (2.5+) use the loop
keyword, though legacy with_*
statements are still supported for backward compatibility.
The basic structure looks like this:
- name: Example loop task
module_name:
parameter: "{{ item }}"
loop:
- item1
- item2
- item3
Behind the scenes, Ansible processes each iteration sequentially, substituting the item
variable with the current list element. This mechanism works with virtually any Ansible module, making loops incredibly versatile for configuration management tasks.
Step-by-Step Implementation Guide
Let’s start with basic loop implementations and progressively move to more complex scenarios.
Simple List Loops
The most straightforward loop iterates over a simple list of values:
- name: Install multiple packages
package:
name: "{{ item }}"
state: present
loop:
- nginx
- mysql-server
- php-fpm
- git
Dictionary Loops
When working with key-value pairs, dictionary loops become essential:
- name: Create users with specific groups
user:
name: "{{ item.key }}"
groups: "{{ item.value.groups }}"
shell: "{{ item.value.shell }}"
state: present
loop: "{{ users | dict2items }}"
vars:
users:
john:
groups: ['sudo', 'developers']
shell: /bin/bash
jane:
groups: ['admin', 'operators']
shell: /bin/zsh
Nested Loops
For scenarios requiring nested iteration, use the loop
with subelements
filter:
- name: Configure firewall rules for multiple servers
firewalld:
port: "{{ item.1 }}"
permanent: yes
state: enabled
zone: "{{ item.0.zone }}"
loop: "{{ servers | subelements('ports') }}"
vars:
servers:
- name: web-server
zone: public
ports: ['80/tcp', '443/tcp']
- name: db-server
zone: internal
ports: ['3306/tcp', '5432/tcp']
Conditional Loops
Combine loops with conditionals for more sophisticated control flow:
- name: Install packages only on specific distributions
package:
name: "{{ item.name }}"
state: present
loop:
- { name: 'apache2', distro: 'Ubuntu' }
- { name: 'httpd', distro: 'CentOS' }
- { name: 'nginx', distro: 'all' }
when: item.distro == ansible_distribution or item.distro == 'all'
Real-World Examples and Use Cases
Database Setup with Multiple Schemas
Here’s a practical example for setting up multiple database schemas:
- name: Create multiple databases and users
mysql_db:
name: "{{ item.db_name }}"
state: present
loop:
- { db_name: 'app_production', user: 'app_prod', password: 'secure_prod_pass' }
- { db_name: 'app_staging', user: 'app_stage', password: 'secure_stage_pass' }
- { db_name: 'app_development', user: 'app_dev', password: 'secure_dev_pass' }
- name: Create database users with privileges
mysql_user:
name: "{{ item.user }}"
password: "{{ item.password }}"
priv: "{{ item.db_name }}.*:ALL"
state: present
loop:
- { db_name: 'app_production', user: 'app_prod', password: 'secure_prod_pass' }
- { db_name: 'app_staging', user: 'app_stage', password: 'secure_stage_pass' }
- { db_name: 'app_development', user: 'app_dev', password: 'secure_dev_pass' }
SSL Certificate Deployment
Deploying SSL certificates across multiple virtual hosts:
- name: Copy SSL certificates for multiple domains
copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: 0600
owner: root
group: root
loop:
- { src: 'certs/example.com.crt', dest: '/etc/ssl/certs/example.com.crt' }
- { src: 'certs/api.example.com.crt', dest: '/etc/ssl/certs/api.example.com.crt' }
- { src: 'certs/admin.example.com.crt', dest: '/etc/ssl/certs/admin.example.com.crt' }
notify: restart nginx
Configuration File Templates
Generating configuration files for multiple services:
- name: Generate service configuration files
template:
src: "{{ item.template }}"
dest: "{{ item.dest }}"
backup: yes
loop:
- { template: 'nginx.conf.j2', dest: '/etc/nginx/nginx.conf' }
- { template: 'php-fpm.conf.j2', dest: '/etc/php/7.4/fpm/php-fpm.conf' }
- { template: 'mysql.cnf.j2', dest: '/etc/mysql/mysql.conf.d/mysqld.cnf' }
notify:
- restart nginx
- restart php-fpm
- restart mysql
Loop Types Comparison
Loop Type | Use Case | Performance | Complexity | Best For |
---|---|---|---|---|
Simple loop | Basic list iteration | Fast | Low | Package installation, file operations |
Dictionary loop | Key-value pair processing | Medium | Medium | User creation, service configuration |
Nested loop | Multi-dimensional data | Slower | High | Complex configurations, rule sets |
Conditional loop | Selective iteration | Variable | Medium | Environment-specific tasks |
Range loop | Numeric sequences | Fast | Low | Port ranges, numbered resources |
Advanced Loop Techniques
Using loop_control
The loop_control
directive provides additional control over loop execution:
- name: Process large dataset with custom loop variable
debug:
msg: "Processing user {{ user_item.name }} with ID {{ user_item.id }}"
loop: "{{ user_database }}"
loop_control:
loop_var: user_item
pause: 2
label: "{{ user_item.name }}"
Performance Optimization with until Loops
For tasks that need to wait for conditions, use until
loops:
- name: Wait for service to be ready
uri:
url: "http://{{ item }}/health"
method: GET
register: health_check
until: health_check.status == 200
retries: 30
delay: 10
loop:
- web-server-1.example.com
- web-server-2.example.com
- web-server-3.example.com
Flattening Complex Data Structures
Use the flatten
filter for nested lists:
- name: Install packages from multiple sources
package:
name: "{{ item }}"
state: present
loop: "{{ [base_packages, web_packages, db_packages] | flatten }}"
vars:
base_packages: ['curl', 'wget', 'vim']
web_packages: ['nginx', 'apache2']
db_packages: ['mysql-server', 'postgresql']
Common Pitfalls and Troubleshooting
Variable Scope Issues
One frequent problem occurs when loop variables conflict with existing variables:
# Problem: 'item' variable conflicts
- name: Problematic nested loop
debug:
msg: "Outer: {{ item }}, Inner: {{ item }}" # Inner loop overwrites outer
loop: "{{ outer_list }}"
# Inner task here would break variable access
# Solution: Use loop_control with custom variable names
- name: Fixed nested loop
debug:
msg: "Server: {{ server_item }}, Port: {{ port_item }}"
loop: "{{ servers }}"
loop_control:
loop_var: server_item
Performance Issues with Large Datasets
Loops can become slow with large datasets. Here are optimization strategies:
- Use
async
andpoll
for parallelizable tasks - Implement batching with
batch
filter - Consider using
block
statements to group related loop tasks - Use
changed_when
andfailed_when
to reduce unnecessary iterations
- name: Process large file list in batches
file:
path: "{{ item }}"
state: absent
loop: "{{ large_file_list | batch(50) | list }}"
async: 300
poll: 0
Error Handling in Loops
Implement proper error handling to prevent single failures from stopping entire loops:
- name: Install packages with error handling
package:
name: "{{ item }}"
state: present
loop: "{{ package_list }}"
register: package_results
failed_when: false
changed_when: package_results.rc == 0
- name: Report failed package installations
debug:
msg: "Failed to install: {{ item.item }}"
loop: "{{ package_results.results }}"
when: item.rc != 0
Best Practices and Security Considerations
Security Best Practices
- Never expose sensitive data in loop labels – use
no_log: true
for sensitive operations - Validate input data before processing in loops
- Use
loop_control.label
to hide sensitive information from logs - Implement proper error handling to prevent information disclosure
- name: Create users with passwords (secure)
user:
name: "{{ item.name }}"
password: "{{ item.password | password_hash('sha512') }}"
state: present
loop: "{{ users }}"
no_log: true
loop_control:
label: "{{ item.name }}"
Performance Best Practices
- Use specific loop types rather than generic ones when possible
- Minimize the use of nested loops – flatten data structures when feasible
- Implement conditional checks early to skip unnecessary iterations
- Use
run_once
for tasks that only need to execute on one host - Consider using
delegate_to
for tasks that should run on specific hosts
Code Maintainability
- Use descriptive variable names in
loop_control.loop_var
- Keep loop logic simple – complex operations should be moved to separate tasks
- Document complex loop structures with comments
- Use variables to store loop data rather than inline definitions
Integration with Other Ansible Features
Loops integrate seamlessly with other Ansible features like handlers, blocks, and roles. Here’s how to leverage these combinations:
- name: Complex loop with block and rescue
block:
- name: Configure services
template:
src: "{{ item.template }}"
dest: "{{ item.dest }}"
loop: "{{ service_configs }}"
notify: "restart {{ item.service }}"
rescue:
- name: Handle configuration failures
debug:
msg: "Service configuration failed, reverting to defaults"
- name: Restore default configurations
copy:
src: "defaults/{{ item.service }}.conf"
dest: "{{ item.dest }}"
loop: "{{ service_configs }}"
For comprehensive documentation on Ansible loops and their various implementations, refer to the official Ansible documentation. The Ansible examples repository also provides extensive real-world implementations you can adapt for your specific use cases.
Mastering loops in Ansible playbooks significantly improves your automation capabilities and code maintainability. Start with simple implementations and gradually incorporate more advanced techniques as your infrastructure automation needs grow. Remember that well-designed loops not only reduce code duplication but also make your playbooks more readable and easier to troubleshoot.

This article incorporates information and material from various online sources. We acknowledge and appreciate the work of all original authors, publishers, and websites. While every effort has been made to appropriately credit the source material, any unintentional oversight or omission does not constitute a copyright infringement. All trademarks, logos, and images mentioned are the property of their respective owners. If you believe that any content used in this article infringes upon your copyright, please contact us immediately for review and prompt action.
This article is intended for informational and educational purposes only and does not infringe on the rights of the copyright owners. If any copyrighted material has been used without proper credit or in violation of copyright laws, it is unintentional and we will rectify it promptly upon notification. Please note that the republishing, redistribution, or reproduction of part or all of the contents in any form is prohibited without express written permission from the author and website owner. For permissions or further inquiries, please contact us.