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!