Loops in the Ansible code – the basics of iteration

Ansible loops and conditional statements are very cool inventions, but they are a double edged sword as well. The limitless freedom they give us can turn our roles into horrible mess. It is our responsibility to balance on the edge and use just the right amount of them in our code while we keep in mind that YAML is not a programming language.

At the beginning of learning Ansible everyone falls in love with the simplicity and the fast results it provide. It really makes a difference in our daily lives, but as our code base starts growing it is more difficult to maintain it.

Let me help you in advance to avoid some possible pitfalls of Ansible: use a minimal amount of conditional statements and use only basic loops. Maybe some of you would argue this statement, and I am always open to listen other opinions, but in enterprise environments loops and conditionals may cause major issues. They slow down the development processes and they create a lot of confusion when we look for errors in the code.

This is because YAML is not a programming language and we cannot write complex logic in YAML without sacrificing Ansible’s biggest strength: its simplicity.

If we want to execute a task multiple times on different values, then we can use a loop.

The package installation in our common role using the ansible.builtin.apt module is a good example of looping through a list of variables. We did not write a task for every package installation, but simply created a YAML list and passed it to the module.

We were lucky because the ansible.builtin.apt module handled the looping for us through its name parameter. It is an efficient way of looping over the packages because the module gives the package list directly to APT.

# Think about using

$ apt install vim mc tmux emacs

# versus the

$ apt install vim && apt install mc && apt install tmux && apt install emacs

Otherwise with using the loop keyword the task would handle the package installation individually for every package.

Our task using the module and the list looked like this in our role:

# The task in the roles/common/tasks/main.yml:
- name: Install the basic packages
  ansible.builtin.apt:
    name: "{{ common_packages }}"
    state: present

# The variable in the roles/common/defaults/main.yml:
common_packages:
  - vim
  - tmux
  - python3
  - python3-apt
  - sshpass
  - git
  - wget
  - curl
  - zsh

This is the most basic looping we can achieve in Ansible handled by the module.

Unfortunately we will run into modules and scenarios where we can rely only on Ansible’s looping keywords.

Ansible offers the loop, with_< lookup >, and until keywords to execute a task multiple times.

Ansible Docs

Since Ansible 2.5 the recommended way of looping through data structures is the loop keyword. In the future the with_<lookup> keywords will be deprecated and removed. They are notably slower and rely on the lookup plugins.

We added loop in Ansible 2.5. It is not yet a full replacement for with_< lookup >, but we recommend it for most use cases.

Ansible Docs

Let’s use the loop keyword and stick to simple data structures whenever it is possible!

An Ansible standard loop looks like this:

- name: Add several users
  ansible.builtin.user:
    name: "{{ item }}"
    state: present
    groups: "wheel"
  loop:
     - testuser1
     - testuser2

We can iterate through a dictionary too, though if we do not use them in a list then we have to use the dict2items filter.

Let’s do some practice and take a look at the structures in a quickly developed test playbook:

---
- name: Test playbook for looping
  hosts: ansible

  tasks:

    - name: Iteration through a list
      ansible.builtin.debug:
        msg: "{{ item }}"
      loop:
        - test1
        - test2

    - name: Iteration through a list of dictionaries
      ansible.builtin.debug:
        msg: "{{ item.first_id }} and {{ item.second_id }}"
      loop:
        - { first_id: test1, second_id: secret1 }
        - { first_id: test2, second_id: secret2 }

    - name: Iteration through a dictionary
      ansible.builtin.debug:
        msg: "{{ item.key }} - {{ item.value }}"
      loop: "{{ users | dict2items }}"
      vars:
        users:
          user1: tamas
          user2: tom

If we run this, then we will see how Ansible handles the different data types:

$ ansible-playbook -i inventory play-test.yml

PLAY [Test playbook for looping] *************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************
ok: [ansible]

TASK [Iteration through a list] **************************************************************************************************************
ok: [ansible] => (item=test1) => {
    "msg": "test1"
}
ok: [ansible] => (item=test2) => {
    "msg": "test2"
}

TASK [Iteration through a list of dictionaries] **********************************************************************************************
ok: [ansible] => (item={'first_id': 'test1', 'second_id': 'secret1'}) => {
    "msg": "test1 and secret1"
}
ok: [ansible] => (item={'first_id': 'test2', 'second_id': 'secret2'}) => {
    "msg": "test2 and secret2"
}

TASK [Iteration through a dictionary] ********************************************************************************************************
ok: [ansible] => (item={'key': 'user1', 'value': 'tamas'}) => {
    "msg": "user1 - tamas"
}
ok: [ansible] => (item={'key': 'user2', 'value': 'tom'}) => {
    "msg": "user2 - tom"
}

PLAY RECAP ***********************************************************************************************************************************
ansible                    : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

We can clearly see that the simple list and the simple list of dictionary elements are easy to use. I strongly recommend to avoid more complex data structures in Ansible. Do not try to write looping code on nested data structures. Complex data structures are not the strength of this simple system.

The loop keyword is simple, fast and efficient, so let’s use it when we need iterations.

We’ve already used iteration over a simple list in our common role when we copied the configuration files to the root’s and the user’s home directories:

# roles/common/tasks/main.yml
- name: Copy the config files to the root user
  ansible.builtin.copy:
    src: '{{ item.src }}'
    dest: '{{ item.dest }}'
    owner: root
    group: root
    mode: '0754'
    remote_src: true
  loop: "{{ common_root_copy_files }}"

# roles/common/defaults/main.yml
common_root_copy_files:
  - { src: /home/tmolnar/stuff/dotfiles/.vimrc, dest: /root/.vimrc }
  - { src: /home/tmolnar/stuff/dotfiles/.tmux.conf, dest: /root/.tmux.conf }
  - { src: /home/tmolnar/stuff/dotfiles/.zshrc, dest: /root/.zshrc }

This is not just a simple list, but the list elements are dictionaries. The loop keyword iterates through every list element like { src: /home/tmolnar/stuff/dotfiles/.vimrc, dest: /root/.vimrc } and it registers the contents in the item variable. In our case it is a dictionary, and there are two items that can be accessed by their IDs, the src and the dest. That’s why we use in our task the item.src and item.dest variables.

We can use simple loops to simplify our Ansible code.

If you have anything to share then please visit my Tom’s IT Cafe Discord Server!

Leave a comment