How to Use Ansible Loops - With Examples

In this blog post, we will cover how to use 'Ansible loops'. We'll start with what 'loops' are by using a simple everyday example, so everyone can understand. Then, we'll explain why we use loops in Ansible and how they can help us. We'll look at the most basic type of loop, and then see a few different kinds. We'll show how these loops are used in real situations. And finally, we'll share some common mistakes to avoid when using loops in Ansible. Let's get started.

Why We Use Loops in Ansible?

Loops in Ansible are like shortcuts for doing the same task many times. Instead of repeating the same steps over and over again, we use a loop to do the task as many times as we need. This saves time and makes our tasks less error-prone because we only have to write the instructions once.

Let's imagine you're setting up a new Linux server. It needs various software packages installed, like 'nginx', 'mysql', and 'php'. Without loops, you would have to write a separate task for installing each package in your Ansible playbook, like this.

- name: Install nginx
  apt: 
    name: nginx 
    state: present
- name: Install mysql
  apt: 
    name: mysql
    state: present
- name: Install php
  apt: 
    name: php
    state: present

This works, but it's repetitive and hard to manage. If you have to install ten or twenty packages, the playbook becomes long and hard to read.

Now, let's use a loop to do the same task. You list all the packages you want to install and then write a single task that loops over the list.

- name: Install Packages
  apt: 
    name: "{{ item }}" 
    state: present
  loop:
    - nginx
    - mysql
    - php

This playbook does exactly the same job as the first one, but it's much shorter and easier to understand. If you need to add more packages, you just add them to the list. The loop will take care of installing each one. I hope you can start to see the power of loops in Ansible.

💡
In Ansible loops, the term {{ item }} is used as a placeholder for each element in the list you're looping through. We will go through this in a lot more detail later in the post.

Iterating over a simple list

Ansible provides an easy and readable way to loop over a set of tasks using the loop keyword. The purpose of a loop is to repeat the same task multiple times, which simplifies the playbook and reduces repetition. Here is the basic syntax of an Ansible loop.

tasks:
  - name: Task description
    ansible_module:
      key: "{{ item }}"
    loop: [item1, item2, item3]
  • ansible_module refers to the module you're using for the task. Ansible has many modules for various tasks, such as apt for managing packages, file for file management, etc.
  • key: "{{ item }}" is where we tell Ansible what to change in each loop. The {{ item }} is a placeholder that Ansible replaces with each item in the loop list.
  • loop is the keyword that starts the loop, followed by the list of items to loop over.

Going back to the previous package installation example, without a loop, we'd need to write a separate task for each package. With a loop, we can install all packages with a single task.

---
- name: Install packages using Ansible
  hosts: servers
  tasks:
    - name: Install Packages
      apt: 
        pkg: "{{ item }}" 
        state: present
      loop:
        - nginx
        - mysql
        - php
  • We're using the apt module, which manages packages on Debian-based systems.
  • The line pkg: "{{ item }}" tells Ansible what to install in each loop iteration. Ansible replaces {{ item }} with each package name from the list.
  • The loop keyword starts the loop, followed by a list of packages we want to install.

When you run this playbook, Ansible will install the 'nginx', 'mysql', and 'php' packages one by one. If you need to install more packages, you simply add them to the list. This is why loops are so powerful in Ansible: they let us manage multiple items with a single task, no matter how many items we have.

Looping over a Dictionary

To loop over this dictionary, you can use the dict2items filter, which converts the dictionary into a list of items. Each item in this list is another dictionary with a 'key' and a 'value'.

---
- name: Assign roles to servers
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Print server role info
      ansible.builtin.debug:
        msg: "Server: {{ item.key }}, Role: {{ item.value }}"
      loop: "{{ servers | dict2items }}"
      vars:
        servers:
          server1: 'web'
          server2: 'database'
  • servers is a dictionary where each key is a server name and the value is the role of that server.
  • We're using the dict2items filter to convert the servers dictionary into a list of items. Each item is a smaller dictionary with a 'key' (the server name) and a 'value' (the role).
  • ansible.builtin.debug is a module that prints messages to the console. We're using it to print out each server and its corresponding role.
  • msg: "Server: {{ item.key }}, Role: {{ item.value }}" is where we tell Ansible what to print. It takes each server name and its role and prints them in a readable format.

When you run this playbook, Ansible will print out a message for each server, showing the role assigned to it. This is a straightforward way to loop over dictionaries in Ansible when each key has a single corresponding value.

➜  ansible_local ansible-playbook loop_dict.yml

PLAY [Assign roles to servers] ********************************************************************************************************

TASK [Print server role info] *********************************************************************************************************
ok: [127.0.0.1] => (item={'key': 'server1', 'value': 'web'}) => {
    "msg": "Server: server1, Role: web"
}
ok: [127.0.0.1] => (item={'key': 'server2', 'value': 'database'}) => {
    "msg": "Server: server2, Role: database"
}

Looping over Nested Lists

Product Filter

you can use the product filter for creating a Cartesian Product of the given lists, which means every item of the first list is paired with every item of the second list. This can be useful in scenarios where you need to perform a task with every possible combination of items from multiple lists. Here's an example using the product filter.

---
- name: Install packages on servers
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Install packages
      ansible.builtin.debug:
        msg: "Installing {{ item.1 }} on {{ item.0 }}"
      loop: "{{ ['server1', 'server2'] | product(['nginx', 'mysql', 'php']) | list }}"
➜  ansible_local ansible-playbook nested_loop_product.yml

PLAY [Install packages on servers] ****************************************************************************************************

TASK [Install packages] ***************************************************************************************************************
ok: [127.0.0.1] => (item=['server1', 'nginx']) => {
    "msg": "Installing nginx on server1"
}
ok: [127.0.0.1] => (item=['server1', 'mysql']) => {
    "msg": "Installing mysql on server1"
}
ok: [127.0.0.1] => (item=['server1', 'php']) => {
    "msg": "Installing php on server1"
}
ok: [127.0.0.1] => (item=['server2', 'nginx']) => {
    "msg": "Installing nginx on server2"
}
ok: [127.0.0.1] => (item=['server2', 'mysql']) => {
    "msg": "Installing mysql on server2"
}
ok: [127.0.0.1] => (item=['server2', 'php']) => {
    "msg": "Installing php on server2"
💡
If you're anything like me, you might've seen the term 'Cartesian Product' and immediately thought, 'Wow, what's that?' - When we talk about the Cartesian product in the context of programming, we usually refer to combining each item of one list (or set) with each item of another list (or set).

Subelements

In Ansible, you can also loop over nested lists using the subelements filter. This is useful when you want to perform a task for each combination of elements in the outer and inner lists. Here's a basic structure of how to use the subelements filter.

---
- name: Install packages on servers
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Install packages
      ansible.builtin.debug:
        msg: "Installing {{ item.1 }} on {{ item.0.name }}"
      loop: "{{ servers | subelements('packages') }}"
      vars:
        servers:
          - name: server1
            packages:
              - nginx
              - mysql
          - name: server2
            packages:
              - httpd
              - postgresql
  • servers is a list of dictionaries. Each dictionary has a 'name' key for the server name and a 'packages' key for the list of packages.
  • We're using the subelements('packages') filter to loop over both the servers and their packages.
  • ansible.builtin.debug is a module that prints messages to the console. We're using it to print out each server-package combination.
  • msg: "Installing {{ item.1 }} on {{ item.0.name }}" is where we tell Ansible what to print. It takes each server name and each package and prints them in a readable format.
➜  ansible_local ansible-playbook nested_list_loop.yml   

PLAY [Install packages on servers] ****************************************************************************************************

TASK [Install packages] ***************************************************************************************************************
ok: [127.0.0.1] => (item=[{'name': 'server1', 'packages': ['nginx', 'mysql']}, 'nginx']) => {
    "msg": "Installing nginx on server1"
}
ok: [127.0.0.1] => (item=[{'name': 'server1', 'packages': ['nginx', 'mysql']}, 'mysql']) => {
    "msg": "Installing mysql on server1"
}
ok: [127.0.0.1] => (item=[{'name': 'server2', 'packages': ['httpd', 'postgresql']}, 'httpd']) => {
    "msg": "Installing httpd on server2"
}
ok: [127.0.0.1] => (item=[{'name': 'server2', 'packages': ['httpd', 'postgresql']}, 'postgresql']) => {
    "msg": "Installing postgresql on server2"

Pausing within a Loop

Sometimes, when performing actions in a loop, we might need to pause between iterations. This could be because we're waiting for a server to restart, for a file to become available, or for any number of reasons. You canloop_control with pause to add a delay between each loop iteration.

---
- name: Pausing within a loop using loop_control
  hosts: localhost
  gather_facts: no
  tasks:
    - name: Print number with pause
      ansible.builtin.debug:
        msg: "{{ item }}"
      loop: [1, 2, 3]
      loop_control:
        pause: 3
  • loop: [1, 2, 3] specifies that we want to loop over the numbers 1, 2, and 3.
  • ansible.builtin.debug module prints the current number.
  • loop_control: pause: 3 pauses for 3 seconds after each task in the loop.

When you run this playbook, Ansible will print each number, pause for 3 seconds, and then proceed to the next iteration of the loop.

Closing Thoughts

So, there you have it. We've learned that Ansible loops are a handy tool that helps us automate repetitive tasks. Please let me know in the comments if you have any questions or feedback.