Configuration file blueprints: Jinja2 templates in the Ansible code

Templating is a huge work power in Ansible when we want to write reusable code. Just imagine about having to create different playbooks and roles for every host that have a slightly different configuration from each other. That would be nonsense. Luckily we have configuration file templates in Ansible to make our life easier, and Infrastructure as Code less complex. We have already used Jinja2 templates in our playbook when we included variables. Let’s investigate what else can we do!

Ansible uses Jinja2 templating to enable dynamic expressions and access to variables and facts. It is easier to understand this functionality through life-like examples. Let’s create some tasks using the template module for setting up the configuration files.

A basic test template

Let’s use our usual test playbook in our Ansible project directory!

Our play-test.yml playbook should look like this:

---
- name: Test playbook for playing with Ansible
  hosts: all

  tasks:
    - name: Test the Ansible template functionality
      ansible.builtin.template:
        src: /home/tmolnar/stuff/projects/ansible_lab/test_template.j2
        dest: /home/tmolnar/stuff/projects/ansible_lab/test_template.txt
        owner: tmolnar
        group: tmolnar
        mode: '0700'

The above task in our Ansible playbook uses the ansible.builtin.template module to define a file template. We set the source Jinja2 template file (with full path) and the destination file we want to output after the templating. The rest of the arguments are the file permissions.

Let’s create a template file on that path! (/home/tmolnar/stuff/projects/ansible_lab/test_template.j2)

{{ ansible_managed }}

Hello World! I am on {{ ansible_facts['hostname'] }}!

In our test_template.j2 Ansible Jinja2 template file we put a built-in variable in the first line. In real environments it shows that our config file is managed by Ansible.

The text “Hello World!” inserted into the output file (/home/tmolnar/stuff/projects/ansible_lab/test_template.txt) without any modification.

Then we write out the info on which host did we generate the file using an Ansible Fact.

When we run the playbook the generated file will look something similar (depending on the hostname, of course):

Ansible managed

Hello World! I am on DebTop!

We can clearly see that the Ansible built-in variables, facts and custom variables are accessible in our templates. Let’s confirm it with creating a set of variables in our playbook, then use it in the Jinja2 template!

The modified playbook:

---
- name: Test playbook for playing with Ansible
  hosts: all

  vars:
    phase: "learning Ansible"
    topic: "templates"

  tasks:
    - name: Test the Ansible template functionality
      ansible.builtin.template:
        src: /home/tmolnar/stuff/projects/ansible_lab/test_template.j2
        dest: /home/tmolnar/stuff/projects/ansible_lab/test_template.txt
        owner: tmolnar
        group: tmolnar
        mode: '0700'

Add the variable to the Jinja2 template file!

{{ ansible_managed }}

Hello World! I am on {{ ansible_facts['hostname'] }}!

I am {{ phase }} and my current topic is the {{ topic }}.

Run the playbook and check the output!

Ansible managed

Hello World! I am on DebTop!

I am learning Ansible and my current topic is the templates.

We’ve confirmed that we can use our own, custom variables in Ansible templates! 😊 With {{ and }} we can use expressions for variable substitution.

We can use different statements with {% and %}.

Conditional statements – if

The if statement in Jinja is comparable with the Python if statement.

We can test in our template if a variable is set or it has a specific value. Then using this info we can place different things in our Ansible managed configuration file.

{% if variable %}
setting = {{ variable }}
{% endif %}

Of course we can use the elif and else branches as well in our if statements.

From the official documentation here is the example.

{% if kenny.sick %}
    Kenny is sick.
{% elif kenny.dead %}
    You killed Kenny!  You bastard!!!
{% else %}
    Kenny looks okay --- so far
{% endif %}

or we can write:

{% if topic == "templates" %}
I am {{ phase }} and my current topic is the {{ topic }}.
{% elif topic == "conditions" %}
I am at {{ phase }}...
{% else %}
We are still not at templates.
{% endif %}

And a more life-like example:

{% if ansible_facts['os_family'] == "Debian" %}
We use {{ ansible_facts['os_family'] }}.
{% endif %}

We can read more information about the if statement here.

Loops

We can write for loops in Jinja to walk lists and dictionaries of data.

{% for user in users %}
- {{ user }}
{% endfor %}

With loops and conditional statements we can achieve almost anything in our Ansible managed configuration files.

This huge power comes with a huge responsibility. Ansible’s strength is its simplicity, and yet we can create complex and unfollowable code very easily.

It is our responsibility to do not use too complex data structures and do not write nested conditions and loops. Ansible is simple, let’s use it as its strength.

---
- name: Test playbook for playing with Ansible
  hosts: all

  vars:
    valid_cmds:
      - cat
      - head
      - tail
      - grep
      - sed

  tasks:
    - name: Test the Ansible template functionality
      ansible.builtin.template:
        src: /home/tmolnar/stuff/projects/ansible_lab/test_template.j2
        dest: /home/tmolnar/stuff/projects/ansible_lab/test_template.txt
        owner: tmolnar
        group: tmolnar
        mode: '0700'
Valid commands:

{% for cmd in valid_cmds %}
- {{ cmd }}
{% endfor %}

The output will be:

Valid commands:

- cat
- head
- tail
- grep
- sed

Templates in Ansible roles

In roles there is a defined directory tree for using templates:

.
β”œβ”€β”€ defaults
β”‚   └── main.yml
β”œβ”€β”€ files
β”œβ”€β”€ handlers
β”‚   └── main.yml
β”œβ”€β”€ meta
β”‚   └── main.yml
β”œβ”€β”€ README.md
β”œβ”€β”€ tasks
β”‚   └── main.yml
β”œβ”€β”€ templates             <<< Here!
β”œβ”€β”€ tests
β”‚   β”œβ”€β”€ inventory
β”‚   └── test.yml
└── vars
    └── main.yml

We keep our templates in our templates directory. We can refer to a template file in a task relative to this directory, so we do not need to use a full path.

One thought on “Configuration file blueprints: Jinja2 templates in the Ansible code

Leave a comment