Introduction
For most people working with technology, writing general loops has become something so natural that you can do in the time between waking up with an hangover, asking yourself "What the f*** happened?", a shower, a bottle of water and a cup of coffee. As a teacher, I know that loops are a control structure that can make programming newcomers bang their heads on the door (great The Cure album, by the way).
In this article, we're gonna talk a little bit about Ansible's loops, their old syntax with with_items
and the new one. But first, an important message from our sponsor Balas Juquinha remembering us all that:
Ansible's not a programming language
As we discover and get along with new technology, some of us tend to converge the solutions of all problems into it, such as:
Managing hundreds (even thousands) of servers
Deploying applications
Checking out if Elvis is still alive
Relationship problems
Overthrow your neo-fascist government
When people discover Ansible, they're usually trying to replace several Bash and/or Python scripts with screaming "FOR THE LOVE OF OXÓSSI, DON'T RUN IT TWICE!" preceded by "#" on top and code quality that would let any SonarQube crying under the shower in fetal position listening to Tim Maia's "Ela Partiu". So they become emotional ending with a nice folder structure, organization, readable YAML code and a tool that not only gets the job done, but also gives them a workflow. Pretty neat, right?
Not always.
Some people are addicted to shell-scripts, so they try to Anshell-script, or Ansibell-script. Here's an example of how to create 10 empty files:
---
- hosts: localhost
connection: local
tasks:
- name: Making Stefano cry
shell: for i in {1..10}; do touch $i; done;
args:
executable: /bin/bash
The problem with the above form is that it isn't idempotent, because there's no checking of the files are already present in the filesystem. Let's rewrite that, shall we?
Solving it with the Ansible way:
---
- hosts: localhost
connection: local
tasks:
- name: Making Stefano happy (the old way)
copy:
content: ""
dest: "{{ item }}"
force: no
mode: 0640
with_sequence: start=1 end=10
With the 2.5 version, Ansible introduced the new loop
syntax. The main difference between both syntaxes is that with_*
is more explicit, while loop
relies on Jinja2 lookups to work. Still not to worry, Ansible's team won't deprecate with_*
in the near future, but it's a good idea to learn the new syntax. Here's the above playbook with it:
---
- hosts: localhost
connection: local
tasks:
- name: Making Stefano happy (the new way)
copy:
content: ""
dest: "{{ item|format }}"
force: no
mode: 0640
loop: "{{ range(1, 11)|list }}"
Is it too late to talk about the basics?
I know, I should've started with it. Sorry people...
---
- hosts: localhost
connection: local
tasks:
- name: Installing several packages (don't do it)
apt:
name: "{{ item }}"
state: present
become: yes
with_items:
- glances
- htop
- tree
Note: this was just for example purposes. The
apt
module recommends that you use a list in thename
argument instead of loops.
This is the simplest, most common loop in Ansible. Ansible comes with an iterator called item
. And this is the newer form:
---
- hosts: localhost
connection: local
tasks:
- name: Installing several packages (don't do it)
apt:
name: "{{ item }}"
state: present
become: yes
loop:
- glances
- htop
- tree
Not much of a difference, huh?
Hashes and dictionaries
You can also work with hashes and dictionaries, as shown below:
---
- hosts: localhost
connection: local
tasks:
- name: (Hashes example)
debug:
msg: "My name is {{ item.name }} and my surname is {{ item.surname }}"
loop:
- { name: "Stefano", surname: "Martins" }
- name: (Dictionaries example) Some Tim Maia albums
debug:
msg: "Year: {{ item.key }} - Album: {{ item.value }}"
loop: "{{ tim_maia_albums | dict2items }}"
vars:
tim_maia_albums:
1970: Tim Maia
1974: Racional Volume 1
1975: Racional Volume 2
Nested loops
Let's suppose you wanna create this folder structure:
.
├── bebeto
│ ├── di_melo
│ ├── joao_do_vale
│ ├── jorge_ben
│ └── tim
├── joao
│ ├── di_melo
│ ├── joao_do_vale
│ ├── jorge_ben
│ └── tim
└── jose
├── di_melo
├── joao_do_vale
├── jorge_ben
└── tim
Using Bash and nested loops, you usually would create it with:
for i in bebeto joao jose; do
mkdir $i;
for j in di_melo joao_do_vale jorge_ben tim; do
mkdir ${i}/${j};
done;
done;
Many people don't know, but you can also have nested loops in Ansible. Check it out:
---
- hosts: localhost
connection: local
tasks:
- name: Creating directories
file:
mode: 0755
path: "{{ ansible_user }}/teste/{{ item[0] }}/{{ item[1] }}"
recurse: yes
state: directory
loop: "{{ ['bebeto', 'joao', 'jose'] | product(['di_melo', 'joao_do_vale', 'jorge_ben', 'tim']) | list }}"
Subelements
Now let's suppose we want to create a different structure, where each user has a different subset of folders. For instance:
.
├── bebeto
│ ├── di_melo
│ └── joao_do_vale
├── joao
│ ├── di_melo
│ └── tim
└── jose
└── tim
To achieve that, we're gonna have to declare a dictionary with lists in it, ending with a playbook similar to this:
---
- hosts: localhost
connection: local
tasks:
- name: Creating directories
file:
mode: 0755
path: "{{ ansible_user }}/teste/{{ item.0.user }}/{{ item.1 }}"
recurse: yes
state: directory
vars:
folders:
- user: bebeto
subfolders:
- di_melo
- joao_do_vale
- user: joao
subfolders:
- di_melo
- tim
- user: jose
subfolders:
- tim
loop: "{{ folders | subelements('subfolders', 'skip_missing=True') }}"
Conclusion
In this not-so-brief article, we talked a little bit about Ansible loops, why fighting with our emotions to not use Ansible as shell-scripting, and finally about some brief code and different use cases that hopefully can improve your playbooks.
Ansible's documentation is one of the best ones I ever used, with plenty examples.
Hope you all enjoyed.
Abraços!
Top comments (0)