Solving the DevOps Infrastructure Dilemma: Enabling developer velocity with control 💡

Register for the webinar here →

Ansible

Ansible Loops : How To Use, Tutorial & Examples

ansible loop

Ansible is an open-source automation tool that simplifies configuration management, orchestration, application deployments, and cloud provisioning and ensures security and compliance. With Ansible, you gain more control over your environment and create consistency in your deployments.

In this article, we will discuss using Ansible loops as a powerful tool to manage and deploy configurations across multiple machines, eliminating the need for manual intervention. By automating these processes, you can redirect your focus to more critical tasks, ensuring continuous system updates without the burden of constant oversight.

What we will cover:

  1. What are Ansible loops?
  2. How to loop over lists in Ansible
  3. How to loop over dictionaries in Ansible
  4. Ansible with_items vs loop
  5. How to loop a block in Ansible?
  6. How to use multiple loops in Ansible? (Nested loops)
  7. Ansible loop control
  8. Conditional loops in Ansible
  9. Best practices for using loops in Ansible

What are Ansible loops?

Loops in Ansible are sets of instructions that automate repeated tasks, making it easier to perform the same action multiple times without manual repetition. They work similarly to other basic programming looping concepts such as for_each or while. Ansible loops can be used, for example, for installing multiple packages, creating numerous users, or modifying a set of files.

When using Ansible loops you also reduce the possibility of human errors, as consistency is guaranteed regardless of the complexity of the task. Ansible’s straightforward syntax and the ability to include detailed logging provide clear visibility into operations, improving troubleshooting and accountability.

There are many ways to use loops throughout Ansible playbooks, for example:

  • Adding conditionals with your loops for more flexibility
  • Looping through lists and dictionaries
  • Nesting your loops to manage complex or hierarchical data structures
  • Using loop controls to customize the way your loops are being run

Let’s dive deeper into those use cases.

How to loop over lists in Ansible

The simplest form of looping in Ansible is to iterate over a list of items, taking each item, inputting it into the playbook task, and running it each time using the input. This comes in handy when you have a long list of packages you want to install across the Linux boxes in your environment. Instead of creating a single task for each one of these packages, you can condense your code and create a list of the packages’ names. 

Note: We will be using a Debian-based distribution for our examples, but the same concept should apply to other distributions.

Example 1 – Deploying multiple packages on a Linux box

Here is a simple example of using Ansible loops to deploy multiple packages on a Linux box:

---
- name: Install apps
  hosts: myhosts
  become: yes
  tasks:
    - name: Install tools
      apt:
        name: "{{ item }}"
        state: latest
      loop:
        - htop
        - wget
        - tree
        - git
        - nodejs

Example 2 – Using loops to create users and copy files

As we’ve seen above, we can easily use lists to install multiple packages across your Linux boxes. However, we can also utilize lists to create users and copy files across different directories. 

The playbook below utilizes lists and loops to ensure multiple users are present throughout the Linux boxes:

---
- name: Create/validate multiple users
  hosts: myhosts
  become: yes
  tasks:
    - name: Ensure users are present
      user:
        name: "{{ item }}"
        state: present
        shell: /bin/bash
      loop:
        - mjordan
        - mmathers
        - pparker

We can also use lists and loops to copy a single file across different directories:

---
- name: Copy a file to multiple locations
  hosts: myhosts
  tasks:
    - name: Copy file
      copy:
        src: ~/myfile.conf
        dest: "{{ item }}"
      loop:
        - /destination1/myfile.conf
        - /destination2/myfile.conf
        - /destination3/myfile.conf

How to loop over dictionaries in Ansible

Using Dictionaries in the Ansible looping feature can be very useful and make your playbooks more efficient and easier to manage. Dictionaries allow you to iterate over key-value pairs and perform operations on multiple items at once with its own set of values. This provides more scalability and flexibility throughout the playbook as you can add more items to your dictionary without changing the logic of your tasks. 

You can also combine dictionaries with different environmental variables using Ansible inventories or facts, making them more dynamic. 

Here are some examples of using dictionaries across your playbooks.

Example 1 – Using the lookup filter

In the following example, we check if multiple services are managed properly.

In the loop, we state that it needs to use the lookup filter to search for the dictionary ‘services’, where you can reference the key and values. You can also set default values if any dictionary values are missing (e.g., mysqld).

---
- name: Validate Services
  hosts: myhosts
  vars:
    services:
      nginx:
        state: started
        enabled: yes
      httpd:
        state: stopped
        enabled: no
      mysqld:
        state: started
  tasks:
    - name: Ensure services are correct
      service:
        name: "{{ item.key }}"
        state: "{{ item.value.state }}"
        enabled: "{{ item.value.enabled | default('yes') }}"
      loop: "{{ lookup('dict', services) }}"

Example 2 – Using the dict2items filter

We can also use dictionaries to assign UIDs to usernames. 

In the following example, we assign all of our key value pairs under vars and use the dict2items filter to transform the dictionary into a list of dictionaries. This is useful when iterating over a dictionary using loops since the loop feature works directly with lists. 

We didn’t use the dict2items filter in the previous example because the lookup filter with dict directly retrieves the dictionary for iteration and allows you to access the key and values without transforming the dictionary to a list as dict2items does.

---
- name: Create user accounts from a dictionary
  hosts: myhosts
  vars:
    users_dict:
      mjordan:
        uid: 1001
        groups: "dev"
      mmathers:
        uid: 1002
        groups: "prod"

  tasks:
    - name: Create user accounts
      user:
        name: "{{ item.key }}"
        uid: "{{ item.value.uid }}"
        groups: "{{ item.value.groups }}"
      loop: "{{ users_dict | dict2items }}"

What is the difference between with_items and loop in Ansible?

Historically, Ansible with_items was used to perform looping functions throughout playbooks. Due to its limitations, loops were introduced in Ansible 2.5.  Even though Ansible’s with_items is still functional and not yet deprecated, according to Ansible’s documentation, loops are the recommended way of introducing looping constructs in your code.

Loops provide much more flexibility than with_items for tasks outside of simple lists.  They can also be used with other Ansible filters and plugins, such as dictionaries, combining lists, conditionals, nested loops, and more. 

Below is a comparison example of using with_items to create users from a simple list and another example of using loops to create users with specific UIDs assigned to each user.

Using with_items:

---
- name: Create users with with_items
  hosts: myhosts
  become: yes
  tasks:
    - name: Create multiple users
      user:
        name: "{{ item }}"
        state: present
        shell: /bin/bash
      with_items:
        - alice
        - bob
        - charlie

Using loops:

Here, item.0 represents the user names and item.1 represents the UID values in the zip filter, assigning mjordan a UID of 1001 and so on.

---
- name: Create users with loop and set specific UIDs
  hosts: myhosts
  become: yes
  tasks:
    - name: Create multiple users with specific UIDs
      user:
        name: "{{ item.0 }}"
        uid: "{{ item.1 }}"
        state: present
        shell: /bin/bash
      loop: "{{ ['mjordan', 'mmathers', 'pparker'] | zip([1001, 1002, 1003]) | list }}"

How to loop a block in Ansible?

Neither loops nor with_items can be used with blocks in Ansible. Alternatively, you could use include_tasks to achieve a similar result and include task files based on specified conditions.

How to use multiple loops in Ansible? (Nested loops)

You can use multiple loops in Ansible by nesting loops within each other. Nested loops are loops within loops that allow you to iterate over complex data structures and perform multiple iterations within a single task. They can be useful if you need to deploy multiple applications across different environments, set up multiple services with distinct configurations, or manage permissions for multiple users. 

Below is a simple example using nested loops with the subelements lookup option, which is needed when working with nested lists. 

Subelements take two arguments. The first is the list of dictionaries in your primary list, and the second is the keys within each dictionary (which includes the “subelements” — the list of items you want to iterate over). This allows you to perform operations on each sub-element related to its parent element in a single loop. Using nested loops in this example reduces the complexity and makes the playbook more readable. 

In this example, we display the key and values of the nested dictionary and query through the dictionary to display the environments with their specific URL:

- name: Display applications for each environments
  hosts: localhost
  vars:
    apps_environments:
      - app: webapp
        environments:
          - env: development
            url: dev.example.com
          - env: production
            url: example.com
      - app: api
        environments:
          - env: development
            url: dev.api.example.com
          - env: production
            url: api.example.com
  tasks:
    - name: Display application for each environment
      debug:
        msg: "Displaying {{ item.0.app }} for {{ item.1.env }} environment with URL {{ item.1.url }}"
      loop: "{{ query('subelements', apps_environments, 'environments') }}"

The nested list will return each application and its URL for development and production, displaying four results:

TASK [Deploy application to environment]

ok: [localhost] => (item=[{'app': 'webapp'}, {'env': 'development', 'url': 'dev.example.com'}]) => {
    "msg": "Deploying webapp to development environment with URL dev.example.com"
}
ok: [localhost] => (item=[{'app': 'webapp'}, {'env': 'production', 'url': 'example.com'}]) => {
    "msg": "Deploying webapp to production environment with URL example.com"
}
ok: [localhost] => (item=[{'app': 'api'}, {'env': 'development', 'url': 'dev.api.example.com'}]) => {
    "msg": "Deploying api to development environment with URL dev.api.example.com"
}
ok: [localhost] => (item=[{'app': 'api'}, {'env': 'production', 'url': 'api.example.com'}]) => {
    "msg": "Deploying api to production environment with URL api.example.com"
}

Ansible loop control

You can use many different options to manage and customize the flow of your Ansible loops.

The key features of the loop_control keyword include:

  • pause This option allows you to pause your loop for a specific time (in seconds) between each iteration. For example, it can be useful when you need to place a timer after the first iteration to the next iteration due to interacting with specific APIs that have rate limits. 
  • index_varThe index_var option lets you access the index of the current loop that is running, which might be helpful when you need to access the position of the current item in the loop. 
  • loop_varThis option allows you to customize the ‘item’ variable name used in the loop. You can use this to improve the readability of your playbook by specifying a familiar and descriptive name. 
  • extended The extended option was added in Ansible version 2.8. Using this option, you can pull additional details of the loop (e.g., the current item’s index and the total number of items), which is helpful for debugging. 

Below is a simple example of how you can use the loop_control pause feature to add a five-second pause between each loop iteration:

---
- name: Loop Control Pause
  hosts: myhosts
  tasks:
    - name: Echo items with a pause
      command: echo "{{ item }}"
      loop:
        - "Item 1"
        - "Item 2"
        - "Item 3"
      loop_control:
        pause: 5 

Here is another example of using the pause and loop_var feature of loop_control to perform a REST API call to a service that limits the number of requests you can make per minute:

---
- name: API calls
  hosts: localhost
  tasks:
    - name: Call the API with a 2 second delay between requests
      uri:
        url: "https://my_example.com/api/{{ item.endpoint }}"
        method: GET
        return_content: yes
        headers:
          Authorization: "Bearer YOUR_API_TOKEN"
      loop:
        - { endpoint: 'users', id: 1 }
        - { endpoint: 'users', id: 2 }
        - { endpoint: 'posts', id: 1 }
      loop_control:
        loop_var: item
        pause: 2
      register: api_response

    - name: Display API response
      debug:
        msg: "API response for {{ item.item.endpoint }} ID {{ item.item.id }}: {{ item.json }}"
      loop: "{{ api_response.results }}"
      loop_control:
        label: "{{ item.item.endpoint }} ID {{ item.item.id }}"

Conditional loops in Ansible

Conditional loops in Ansible allow you to control the loop iteration and under what conditions it should continue. They are very useful in tailoring your playbook to execute tasks dynamically and ensuring specific actions are run only when the conditional criteria are met. 

If you are familiar with regular conditionals in Ansible, you know that you can utilize the when statement for many situations. The same applies for loops. You could use the when statement in loops to filter items based on their attributes or values and specific variables and skip iterations if the results from the previous run did not meet the conditional criteria. 

Here is an example of using the conditional loops with the when statement to retrieve the OS of the machine you are running against using ansible_facts, enable nginx service and disable httpd service on Debian-based OS:

- name: Manage Services on Debian OS
  hosts: myhosts
  vars:
    services:
      - name: nginx
        enabled: true
      - name: httpd
        enabled: false
  tasks:
    - name: Ensure services are enabled or disabled accordingly
      service:
        name: "{{ item.name }}"
        enabled: "{{ item.enabled }}"
      loop: "{{ services }}"
      when: ansible_facts['os_family'] == "Debian" and item.enabled

In the next example, we will be using the conditional loops to find out if the packages in the loop are installed, and if they are not installed, the playbook will install the package:

---
- name: Ensure packages are installed
  hosts: localhost
  become: yes
  tasks:
    - name: Check if package is installed
      command: dpkg -l | grep "^ii" | grep "{{ item }}"
      loop:
        - "nginx"
        - "curl"
        - "git"
      register: package_check
      ignore_errors: true #ensures loop runs if package is not installed

    - name: Install package if not installed
      apt:
        name: "{{ item.item }}"
        state: present
      loop: "{{ package_check.results }}"
      when: item.rc != 0

Best practices for using loops in Ansible

You should consider the following best practices when running loops in Ansible:

  1. Minimize tasks inside loops — To ensure your loops run cleanly, try not to overburden your loop task with extras and place the necessary tasks within the loop to reduce execution time.
  2. Use failed_when and changed_when These options may be useful in scenarios when tasks don’t have a straightforward success/failure condition, as they can ensure idempotency and give predictable outcomes.
  3. Use when properly — Place the when condition outside of your loops, as it can cause unnecessary iterations if not used correctly. 
  4. Limit nesting levels — Make sure your nested loops are not too deep, as they can become difficult to read and maintain. Consider spreading them into multiple tasks or utilize include_tasks to break up the logic into smaller, manageable pieces. 
  5. Use the loop_control features — The loop_contol option contains way too many useful features to ensure your playbook outputs are clean and easy to debug. 
  6. Don’t use ignore_errors in loops all the time — Make sure you test and know where you are applying this statement, as it can cause critical errors to be overlooked during playbook runs.
  7. Keep it simple — Ensure the conditionals and nested loops in your playbook are neat and clean. If not managed properly, your playbook will eventually become unreadable and difficult to debug during issues.

Ansible + Spacelift

Spacelift’s vibrant ecosystem and excellent GitOps flow can greatly assist you in managing and orchestrating Ansible. By introducing Spacelift on top of Ansible, you can then easily create custom workflows based on pull requests and apply any necessary compliance checks for your organization. Another great advantage of using Spacelift is that you can manage different infrastructure tools like Ansible, Terraform, Pulumi, AWS CloudFormation, and even Kubernetes from the same place and combine their Stacks with building workflows across tools.

If you want to learn more about Spacelift working with Ansible, check our documentation, read our Ansible guide or book a demo with one of our engineers.

Key points

Loops are very handy in Ansible as they can minimize your code and create more efficiency. They are great for automating repetitive tasks, managing complex configurations, and implementing conditionals in your playbooks. As we covered in this article, we can use loops to iterate over lists, dictionaries, handling nested loops, applying specific conditionals, or utilizing loop controls and all of its features, we can harness the true power and flexibility of automation in Ansible. For debugging purposes, it is important that we ensure our playbooks are clean, readable, and not overly complicated using conditionals and nested loops.

Manage Ansible Better with Spacelift

Spacelift helps you manage the complexities and compliance challenges of using Ansible. It brings with it a GitOps flow, so your infrastructure repository is synced with your Ansible Stacks, and pull requests show you a preview of what they’re planning to change. It also has an extensive selection of policies, which lets you automate compliance checks and build complex multi-stack workflows.

Learn More

Struggling to balance developer velocity with control?

Attend the June 25 webinar:

Solving the DevOps Infrastructure Dilemma

Register for the webinar