BLOG POSTS
Using Loops in Ansible Playbooks

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 and poll for parallelizable tasks
  • Implement batching with batch filter
  • Consider using block statements to group related loop tasks
  • Use changed_when and failed_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.

Leave a reply

Your email address will not be published. Required fields are marked