Ansible Tips: How to Use a Loop to Render Multiple Templates

DevOps Ansible

Once you’ve discovered the power of templates in Ansible (and if you haven’t, you should), you’ll start seeing uses for it everywhere!

And once you start to use templates a lot, you’ll want to know how to loop over templates and process them.

So how do you use a template to create several files on a server, in one single task?

In Ansible, we can use the loop feature to do this:

Sometimes you want to repeat a task multiple times … Ansible offers two keywords for creating loops: loop and with_lookup

So, it used to be that you’d use with_lookup (e.g. with_items ) to loop over several items.

But now, the recommendation is that you use loop. As it says in the Ansible docs:

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

Let’s have a look at how to set up loop and template to play nicely together.

First, refresh yourself on templates

As a refresher, let’s see what a simple template task looks like.

This example uses the template file isso.cfg.j2 to render a file called /opt/isso/config/isso.cfg on the target server.

The source file is a Jinja2 template. It looks like a normal file, but it also has some placeholders.

When Ansible processes the template, it will replace the placeholders with real values, and create the target file:

- name: Copy myapp.conf
  template: 
    src: myapp.cfg.j2 
    dest: /opt/myapp/myapp.cfg 

So this is a basic template example. It has a src and a dest.

So what if you want to do this several times? It might be easy if you’re only copying 2 or 3 files, but what if you have hundreds?

We want to replace these with values from a list. How do we do that?

It’s all about loops and hashes

We can use the loop construct in Ansible to iterate over a “list of hashes”.

That’s Ansible-speak for a list of items that have key-value pairs, like src=xxx and dest=yyy.

Each list item (or “hash”) being processed in the loop, will be available to the task, using the variable name item.

So let’s see how it’s done.

Building the solution, step by step

Let’s start with a playbook. I’m just going to create one play, which targets localhost.

(Remember a play in Ansible consists of the hosts to target, and at least one task.)

---
- hosts: localhost

Now, within this play, declare your variables. These will be injected into your templates. I’m going to define values for myusername, mypassword and anothervar.

(Keep these indents exactly as you see them.)

  vars:
    myusername: hello
    mypassword: goodbye
    anothervar: foo

Next, define the list of tasks in this play. In this example, I’m just going to define just one task, using the template module.

The item variable seems new, but this is what the loop module will provide for us, when it processes each item.

  tasks:
  - name: ensure all template files are rendered
    template:
      src: '{{ item.src }}'
      dest: '{{ item.dest }}'

And finally, get loopy. Add a loop section to this task. In this section, we’ll declare a list. Each list item contains the name of a template file, and the location where it should be saved on the remote server.

    loop:
      - { src: 'templates/myapp.cfg.j2', dest: '/home/jeff/myapp.cfg' }
      - { src: 'templates/something.xml.j2', dest: '/home/jeff/something.xml' }

What the finished code looks like

When you’ve finished, you should have a simple playbook that looks like this, containing one Ansible task. The task will loop several times, and process each template in turn:

# A Playbook consisting of 1 Play
---
- hosts: localhost

  # Declare the variables we want to use here - set their names and values
  # These will be used in the task
  vars:
    myusername: hello
    mypassword: goodbye
    anothervar: foo

  tasks:
  - name: ensure all template files are rendered
    # Configure the template module to use dynamic values from the loop
    template:
      src: '{{ item.src }}'
      dest: '{{ item.dest }}'
    # Define a 'list of hashes' for the loop module to iterate over
    # Each item has a 'src' and a 'dest'
    loop:
      - { src: 'templates/myapp.cfg.j2', dest: '/home/jeff/myapp.cfg' }
      - { src: 'templates/something.xml.j2', dest: '/home/jeff/something.xml' }

I’m setting the variables explicitly here (the vars bit), so that you can see the whole example in one code block.

In a real world scenario, you’d probably use one of the other ways to set your variables.

Run the example

Now I can run the playbook, with the ansible-playbook command:

ansible-playbook my-playbook.yml

When it runs, the template files will be rendered and written to their respective locations (set in the dest property).

So what do the example files look like? How do you use variables in a Jinja2 template?

What about the template files?

If you want to see what my template files look like, well, they’re nothing special :)

The templates just contain the variable name, surrounded by braces {{ }}.

These braces define the locations where Ansible should inject the values of my vars.

Here’s templates/myapp.cfg.j2:

[general]
username = {{ myusername }}
password = {{ mypassword }}

When Ansible runs, this will be combined with my variables, to produce this:

[general]
username = hello
password = goodbye

The second example is templates/something.xml.j2:

<hello>{{ anothervar }}</hello>

And this gets rendered as:

<hello>foo</hello>

Summing up

Now you’ve seen how to use Ansible to create multiple files on your target host using templates.

Remember that templates allow you to create files on the server dynamically, using the contents of variables. So you don’t have to create a bunch of files with hard-coded paths and settings.

Happy Ansibleing!

Ansibil-ising.

Ansi-billing.

Or something like that.

Comments

Got any thoughts on what you've just read? Sign in with your GitHub account and leave a comment.