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:
- What are Ansible loops?
- How to loop over lists in Ansible
- How to loop over dictionaries in Ansible
- Ansible with_items vs loop
- How to loop a block in Ansible?
- How to use multiple loops in Ansible? (Nested loops)
- Ansible loop control
- Conditional loops in Ansible
- Best practices for using loops in Ansible
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.
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
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 }}"
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 }}"
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.
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"
}
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_var
— Theindex_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_var
— This 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
— Theextended
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 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
You should consider the following best practices when running loops in Ansible:
- 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.
- Use
failed_when
andchanged_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. - Use
when
properly — Place thewhen
condition outside of your loops, as it can cause unnecessary iterations if not used correctly. - 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.
- Use the
loop_control
features — Theloop_contol
option contains way too many useful features to ensure your playbook outputs are clean and easy to debug. - 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. - 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.
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.
Our latest Ansible enhancements solve three of the biggest challenges engineers face when they are using Ansible:
- Having a centralized place in which you can run your playbooks
- Combining IaC with configuration management to create a single workflow
- Getting insights into what ran and where
Provisioning, configuring, governing, and even orchestrating your containers can be performed with a single workflow, separating the elements into smaller chunks to identify issues more easily.
Would you like to see this in action – or just want a tl;dr? Check out this video I put together showing you Spacelift’s new Ansible functionality:
If you want to learn more about using Spacelift with Ansible, check our documentation, read our Ansible guide, or book a demo with one of our engineers.
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
Managing large-scale playbook execution is hard. Spacelift enables you to automate Ansible playbook execution with visibility and control over resources, and seamlessly link provisioning and configuration workflows.