How to use Jinja2 templates in Ansible with examples

Jinja2 is a very popular and powerful Python-based template engine. Since Ansible is written in Python, it becomes the default choice for most users, just like other Python-based configuration management systems, such as Fabric and SaltStack. The name Jinja originated from the Japanese word for temple, which is similar in phonetics to the word template.

Some of the important features of Jinja2 are:

  • It is fast and compiled just in time with the Python byte code
  • It has an optional sandboxed environment
  • It is easy to debug
  • It supports template inheritance

 

Variables

As we have seen, we can print variable content simply with the '{{ VARIABLE_NAME }}' syntax. If we want to print just an element of an array we can use '{{ ARRAY_NAME['KEY'] }}', and if we want to print a property of an object, we can use '{{ OBJECT_NAME.PROPERTY_NAME }}'.

For example here I have a playbook jinja2_temp_1.yml where I have defined a variable inside the playbook using vars:

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     my_name: Deepak Prasad
   tasks:
   - name: Print message
     debug:
       msg: "My name is {{ my_name }}"

Let us execute this playbook:

[ansible@controller ~]$ ansible-playbook jinja2_temp_1.yml

PLAY [Data Manipulation] *************************************************************************************************

TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
    "msg": "My name is Deepak Prasad"
}

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

So the variable was accessed using {{ my_name }} where {{ }} is referred as Jinja2 syntax. This was a simple method to access any variable inside the playbook.

 

Use built-in filters

Filters are same as small functions, or methods, that can be run on the variable. Some filters operate without arguments, some take optional arguments, and some require arguments. Filters can be chained together, as well, where the result of one filter action is fed into the next filter and the next. Jinja2 comes with many built-in filters

A filter is applied to a variable by way of the pipe symbol (|) followed by the name of the filter and then any arguments for the filter inside parentheses. There can be a space between the variable name and the pipe symbol as well as a space between the pipe symbol and the filter name.

 

Syntax filter

From time to time, we may want to change the style of a string a little bit, without writing specific code for it, for example, we may want to capitalize some text. To do so, we can use one of Jinja2's filters, such as: '{{ VARIABLE_NAME | capitalize }}'. There are many filters available for Jinja2 which can be collected from official Jinja website.

We will go through some of the examples to perform data manipulation using filters in this next playbook jinja2_temp_2.yml:

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     my_name: Deepak Prasad
   tasks:
   - name: Print message
     debug:
       msg:
        - "My name is {{ my_name }}"
        - "My name is {{ my_name | lower }}"
        - "My name is {{ my_name | upper }}"
        - "My name is {{ my_name | capitalize }}"
        - "My name is {{ my_name | title }}"

Here we are printing the same message using different filter. The filter will be applied only to the variable which we have defined under vars i.e my_name

Let us execute this playbook:

[ansible@controller ~]$ ansible-playbook jinja2_temp_2.yml

PLAY [Data Manipulation] *************************************************************************************************

TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
    "msg": [
        "My name is Deepak Prasad",
        "My name is deepak prasad",
        "My name is DEEPAK PRASAD",
        "My name is Deepak prasad",
        "My name is Deepak Prasad"
    ]
}

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

You can observe the difference in the output where "Deepak Prasad" value is manipulated based on the filter applied.

 

default filter

The default filter is a way to provide a default value for an otherwise undefined variable, which will prevent Ansible from generating an error. It is shorthand for a complex if statement checking if a variable is defined before trying to use it, with an else clause to provide a different value.

In this sample playbook jinja2_temp_3.yml I have defined first_name variable but have not defined last_name

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     first_name: Deepak
   tasks:
   - name: Print message
     debug:
       msg:
        - "My name is {{ first_name }} {{ last_name }}"

So, when I execute this playbook, I get this error:

TASK [Print message] *****************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'last_name' is undefined\n\nThe error appears to be in '/home/ansible/jinja2_temp_3.yml': line 8, column 6, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n   tasks:\n   - name: Print message\n     ^ here\n"}

This error is pretty much expected because it says "The task includes an option with an undefined variable"

 

Now we can handle such situations by defining a default filter with the variable. I have updated my playbook and added a default filter with last_name so that if this variable is not defined then the default value will be used:

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     first_name: Deepak
   tasks:
   - name: Print message
     debug:
       msg:
        - "My name is {{ first_name }} {{ last_name | default('Prasad') }}"

Now if we execute this playbook we get proper value for last_name variable:

[ansible@controller ~]$ ansible-playbook jinja2_temp_3.yml

PLAY [Data Manipulation] *************************************************************************************************

TASK [Print message] *****************************************************************************************************
ok: [localhost] => {
    "msg": [
        "My name is Deepak Prasad"
    ]
}

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

List and set

We can use inbuilt filters to iterate over the individual values of List and perform operation such as find the highest number or lowest number within a List. Let us take some examples:

In this sample playbook we are performing a bunch of operations on the list which I have defined under my_vars:

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     my_list: [1,2,3,4,5,6,5,3,7,1,9]
   tasks:
   - name: List and Set
     debug:
       msg:
        - "The highest no {{ my_list | max }}"
        - "The lowest no is {{ my_list | min }}"
        - "Print only unique values {{ my_list | unique }}"
        - "Print random no {{ my_list | random }}"
        - "Join the values of list {{ my_list | join('-') }}"

Let us check the output once we execute this playlist:

TASK [List and Set] ******************************************************************************************************
ok: [localhost] => {
    "msg": [
        "The highest no 9",
        "The lowest no is 1",
        "Print only unique values [1, 2, 3, 4, 5, 6, 7, 9]",
        "Print random no 1",
        "Join the values of list 1-2-3-4-5-6-5-3-7-1-9"
    ]
}

Here using Jinja2 filters we are iterating over the individual values of my_list and then

  • printing only the highest number using max
  • printing only the lowest number using min
  • printing only the unique values using unique
  • printing a random value from the list using random
  • Joining the list values with hyphen(-) and printing the output.

 

Filters dealing with pathnames

Configuration management and orchestration frequently refers to path names, but often only part of the path is desired. Ansible provides a few filters to help.

In this sample playbook jinja2_temp_5.yml I have used multiple filters on Linux and Windows PATH:

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     path1: "/opt/custom/data/bin/script.sh"
     path2: 'C:\Users\deeprasa\PycharmProjects\elasticsearch\test.log'
     path3: "~/jinja2_temp_5.yml"
   tasks:
   - name: filters to work on pathnames
     debug:
       msg:
        - "Linux Path: {{ path1 | dirname }}"
        - "Windows Path: {{ path2 | win_dirname }}"
        - "Linux script name: {{ path1 | basename }}"
        - "Split the path: {{ path2 | win_splitdrive }}"
        - "Windows Drive: {{ path2 | win_splitdrive | first }}"
        - "Windows File name: {{ path2 | win_splitdrive | last }}"
        - "Show Full path: {{ path3 | expanduser }}"

Output from the playbook:

[ansible@controller ~]$ ansible-playbook jinja2_temp_5.yml

PLAY [Data Manipulation] *************************************************************************************************

TASK [filters to work on pathnames] **************************************************************************************
ok: [localhost] => {
    "msg": [
        "Linux Path: /opt/custom/data/bin",
        "script.sh",
        "Split the path: ('C:', '\\\\Users\\\\deeprasa\\\\PycharmProjects\\\\elasticsearch\\\\test.log')",
        "Windows Drive: C:",
        "Windows File name: \\Users\\deeprasa\\PycharmProjects\\elasticsearch\\test.log",
        "Show Full path: /home/ansible/jinja2_temp_5.yml"
    ]
}

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

Filters for date and time

We have different filters to work with date and time.

---
 - name: Data Manipulation
   hosts: localhost
   gather_facts: false
   vars:
     mydate1: "2020-08-14 20:00:00"
     mydate2: "2018-08-15 21:01:40"
   tasks:
   - name: Date and time filters
     debug:
       msg:
        - "Today's date: {{ '%d-%m-%Y' | strftime }}"
        - "Today's date and time: {{ '%d-%m-%Y %H:%M:%S' | strftime }}"
        - "Print seconds since {{ mydate1 }}: {{ ((mydate2 | to_datetime) - (mydate1 | to_datetime)).seconds }}"
        - "Print days since {{ mydate2 }}: {{ ((mydate2 | to_datetime) - (mydate1 | to_datetime)).days }}"

In this playbook jinja2_temp_6.yml we are simply printing the date and time in the first two debug message while in the next two we are converting the time passed since the provided date into seconds and days.

Let us execute this playbook:

[ansible@controller ~]$ ansible-playbook jinja2_temp_6.yml

PLAY [Data Manipulation] *************************************************************************************************

TASK [Date and time filters] *********************************************************************************************
ok: [localhost] => {
    "msg": [
        "Today's date: 24-09-2020",
        "Today's date and time: 24-09-2020 06:53:45",
        "Print seconds since 2020-08-14 20:00:00: 3700",
        "Print days since 2018-08-15 21:01:40: -730"
    ]
}

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

Configure VSFTPD using Jinja2 templates

Now that we know something about Jinja2 filters and syntax, we will use a Jinja2 template so configure vsftpd on one of our managed nodes.

I will create lab2 project directory which we will use for this example. Here Project means nothing but a new directory which contains everything your playbook needs such as ansible.cfg, inventory etc.

[ansible@controller ~]$ mkdir lab2
[ansible@controller ~]$ cd lab2/

Next copy the ansible.cfg from the default location to the project directory

[ansible@controller lab2]$ cp /etc/ansible/ansible.cfg .

We will create our own inventory file with single managed node entry as I don't need multiple managed nodes for this example:

[ansible@controller lab2]$ cat inventory
server2

Create a templates directory and navigate inside the same:

[ansible@controller lab2]$ mkdir templates
[ansible@controller lab2]$ cd templates/

We have created a Jinja2 template file with content which is required to configure an FTP server using vsftpd. I have also used ansible facts to get the IPv4 address from the managed node and place it in the vsftpd.conf just for reference purpose.

[ansible@controller templates]$ cat vsftpd.j2
anonymous_enable={{ anonymous_enable }}
local_enable={{ local_enable }}
write_enable={{ write_enable }}
anon_upload_enable={{ anon_upload_enable }}
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address={{ ansible_facts['default_ipv4']['address'] }}

Now that our Jinja2 template is ready, we will create our playbook configure_vsftpd.yml inside the lab2 directory which will install and configure vsftpd on server2. So this playbook will perform 2 TASK.

[ansible@controller lab2]$ cat configure_vsftpd.yml
---
 - name: Install and Configure vSFTPD
   hosts: server2
   become: yes
   vars:
     anonymous_enable: yes
     local_enable: yes
     write_enable: yes
     anon_upload_enable: yes
   tasks:
     - name: install vsftp
       yum:
         name: vsftpd
     - name: use Jinja2 template to configure vsftpd
       template:
         src: vsftpd.j2
         dest: /etc/vsftpd/vsftpd.conf

We will now execute the playbook:

[ansible@controller lab2]$ ansible-playbook configure_vsftpd.yml

PLAY [Install and Configure vSFTPD] **************************************************************************************

TASK [Gathering Facts] ***************************************************************************************************
ok: [server2]

TASK [install vsftp] *****************************************************************************************************
changed: [server2]

TASK [use Jinja2 template to configure vsftpd] ***************************************************************************
changed: [server2]

PLAY RECAP ***************************************************************************************************************
server2                    : ok=3    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

So the playbook has executed successfully and changed=2 which means both our task have completed.

Let us verify the vsftpd.conf content from server2

[ansible@server2 ~]$ sudo cat /etc/vsftpd/vsftpd.conf
anonymous_enable=True
local_enable=True
write_enable=True
anon_upload_enable=True
dirmessage_enable=YES
xferlog_enable=YES
connect_from_port_20=YES
pam_service_name=vsftpd
userlist_enable=YES
# MY IP Address=172.31.23.18

 

What's Next

Next in our Ansible Tutorial we will learn all about Ansible Facts

 

Leave a Comment

Please use shortcodes <pre class=comments>your code</pre> for syntax highlighting when adding code.