DEV Community

Stefano Martins
Stefano Martins

Posted on • Edited on

Talking a little bit about Ansible's loops

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?

image

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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 }}"


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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:



--- 
- hosts: localhost
  connection: local
  tasks:
    - name: Installing several packages (don't do it)
      apt:
        name: "{{ item }}"
        state: present
      become: yes
      loop: 
        - glances
        - htop
        - tree


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

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 }}"


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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') }}"

Enter fullscreen mode Exit fullscreen mode




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)