Talking a little bit about Ansible's loops
Tags: ansible, sre, devops
Published: 2021-04-19 02:53:06

## 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](http://juquinha.com.br/) 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?

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/err8lzdfelfzlf6r0dbz.png)

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:

```yaml
--- 
- 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:

```yaml
--- 
- 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:

```yaml
--- 
- 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...

```yaml
--- 
- 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 the `name` 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:

```yaml
--- 
- 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:

```yaml
--- 
- 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:

```textile
.
├── 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:

```bash
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:

```yaml
--- 
- 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:

```textile
.
├── 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:

```yaml
--- 
- 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!