Ansible - Simple Inventory and Host Iteration with Jinja2

Recently I had cause to revisit a topic that often seems to cause problems for people coming to Ansible for the first time, especially for people (like me) who don’t have a development background. How to iterative over inventory variables or facts using a simple Jinja2 template.

It can be a fussy task to get your head around and the documentation isn’t the greatest to the newcomer, so this is going to be a very short post to try and break down the issue with as little jargon as possible.

What’s The Problem? A Real World Scenario

One of the most common things we want to do with Ansible is build a configuration file for a remote server. Ideally, we want to do this dynamically based on data from our Inventory but accessing and iterating over the inventory can be a little tricky to understand.

In this example, we’ll be working with an inventory with 5 hosts named webserver01 - webserver05:

#--inventory.yaml

---
all:
  hosts:
    webserver[01:05]
    
  children:
    webservers:
      webserver[01:05]
    
  vars:
    system_domain: tinfoilcipher.co.uk
...

For our demonstration, we’re going to be creating a haproxy configuration file, which is going to need to end up looking like this:

#--haproxy.cfg

defaults
    mode    http
    timeout check 10s

frontend http
    bind *:443
    mode tcp
    default_backend application

backend application
    mode tcp
    balance     roundrobin
    server webserver01.tinfoilcipher.co.uk 10.0.1.10:8080 check fall 3 rise 2
    server webserver02.tinfoilcipher.co.uk 10.0.1.11:8080 check fall 3 rise 2
    server webserver03.tinfoilcipher.co.uk 10.0.1.12:8080 check fall 3 rise 2
    server webserver04.tinfoilcipher.co.uk 10.0.1.13:8080 check fall 3 rise 2
    server webserver05.tinfoilcipher.co.uk 10.0.1.14:8080 check fall 3 rise 2

So how do we get this file?

The Solution

Obviously we can look up the hostname of our servers using the ansible host variable. However we can’t just use that in a play. On the surface it might look that way but if we do we’ll just end up with a config file with file that has the same entry 5 times because we aren’t looping over anything, this is where using a Jinja2 template comes in. We can also look up the IP address of each node by making use of Ansible facts, allowing us to dynamically look up the IP address of each server at run time.

First we need to create a separate template file, named haproxy.cfg.j2 (this is just the name of whatever file you’re trying to create with an additional .j2 suffix). In there we’ll add some templating logic:

#--haproxy.cfg.j2

defaults
    mode    http
    timeout check 10s

frontend http
    bind *:443
    mode tcp
    default_backend application

backend application
    mode tcp
    balance     roundrobin

{% for host in groups['webservers'] %}
    server {{ host }}.{{ system_domain }} {{ hostvars[host].ansible_default_ipv4.address }}:8080 check fall 3 rise 2
{% endfor %}

We can then reference this file using an Ansible task:

---
- name: Build Config Files
  hosts: webservers
  gather_facts: true
  any_errors_fatal: 

  tasks:
  - name: Build haproxy.cfg
    template:
      src: haproxy.cfg.j2
      dest: haproxy.cfg
...

This will produce exactly the configuration file that we’re looking for!

Written on August 14, 2023