Ansible

RHCE Ansible Series #7: Jinja2 Templates

Learn how to use Jinja2 templating engine to carry out more involved and dynamic file modifications with Ansible.

Ahmed Alkabary
Ahmed Alkabary

Table of Contents

In the previous tutorial about decision making in Ansible, you learned how to do simple file modifications by using the blockinfile or inline Ansible modules.

In this tutorial, you will learn how to use Jinja2 templating engine to carry out more involved and dynamic file modifications.

You will learn how to access variables and facts in Jinja2 templates. Furthermore, you will learn how to use conditional statements and loop structures in Jinja2.

To try the examples in this tutorial, you should follow the entire RHCE Ansible tutorial series in the correct order.

Accessing Variables in Jinja2

Ansible will look for jinja2 template files in your project directory or in a directory named templates under your project directory.

Let’s create a templates directory to keep thing cleaner and more organized:

[[email protected] plays]$ mkdir templates
[[email protected] plays]$ cd templates/

Now create your first Jinja2 template with the name index.j2:

[[email protected] templates]$ cat index.j2 
A message from {{ inventory_hostname }}
{{ webserver_message }}

Notice that Jinja2 template filenames must end with the .j2 extension.

The inventory_hostname is another Ansible built-in (aka special or magic) variable that references that ‘current’ host being iterated over in the play. The webserver_message is a variable that you will define in your playbook.

Now go one step back to your project directory and create the following check-apache.yml:

[[email protected] plays]$ cat check-apache.yml 
---
- name: Check if Apache is Working
  hosts: webservers
  vars:
    webserver_message: "I am running to the finish line."
  tasks:
    - name: Start httpd
      service:
        name: httpd
        state: started

    - name: Create index.html using Jinja2
      template:
        src: index.j2
        dest: /var/www/html/index.html

Note that the httpd package was already installed in a previous tutorial.

In this playbook, you first make sure Apache is running in the first task Start httpd. Then use the template module in the second task Create index.html using Jinja2to process and transfer the index.j2 Jinja2 template file you created to the destination /var/www/html/index.html.

Go ahead and run the playbook:

[[email protected] plays]$ ansible-playbook check-apache.yml 

PLAY [Check if Apache is Working] **********************************************

TASK [Gathering Facts] *********************************************************
ok: [node3]
ok: [node2]

TASK [Start httpd] *************************************************************
ok: [node2]
ok: [node3]

TASK [Create index.html using Jinja2] ******************************************
changed: [node3]
changed: [node2]

PLAY RECAP *********************************************************************
node2                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    
node3                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0

Everything looks good so far; let’s run a quick ad-hoc Ansible command to check the contents of index.html on the webservers nodes:

[[email protected] plays]$ ansible webservers -m command -a "cat /var/www/html/index.html"
node3 | CHANGED | rc=0 >>
A message from node3
I am running to the finish line.
node2 | CHANGED | rc=0 >>
A message from node2
I am running to the finish line.

Amazing! Notice how Jinja2 was able to pick up the values of the inventory_hostname built-in variable and the webserver_message variable in your playbook.

You can also use the curl command to see if you get a response from both webservers:

[[email protected] plays]$ curl node2.linuxhandbook.local
A message from node2
I am running to the finish line.
[[email protected] plays]$ curl node3.linuxhandbook.local
A message from node3
I am running to the finish line.

Accessing Facts in Jinja2

You can access facts in Jinja2 templates the same way you access facts from your playbook.

To demonstrate, change to your templates directory and create the info.j2 Jinja2 file with the following contents:

[[email protected] templates]$ cat info.j2 
Server Information Summary
--------------------------

hostname={{ ansible_facts['hostname'] }}
fqdn={{ ansible_facts['fqdn'] }}
ipaddr={{ ansible_facts['default_ipv4']['address'] }}
distro={{ ansible_facts['distribution'] }}
distro_version={{ ansible_facts['distribution_version'] }}
nameservers={{ ansible_facts['dns']['nameservers'] }}
totalmem={{ ansible_facts['memtotal_mb'] }}
freemem={{ ansible_facts['memfree_mb'] }}

Notice that info.j2 accesses eight different facts. Now go back to your project directory and create the following server-info.yml playbook:

[[email protected] plays]$ cat server-info.yml 
---
- name: Server Information Summary
  hosts: all
  tasks:
   - name: Create server-info.txt using Jinja2
     template:
       src: info.j2
       dest: /tmp/server-info.txt

Notice that you are creating /tmp/server-info.txt on all hosts based on the info.j2 template file. Go ahead and run the playbook:

[[email protected] plays]$ ansible-playbook server-info.yml 

PLAY [Server Information Summary] *******************************************

TASK [Gathering Facts] **********************************
ok: [node4]
ok: [node1]
ok: [node3]
ok: [node2]

TASK [Create server-info.txt using Jinja2] ********
changed: [node4]
changed: [node1]
changed: [node3]
changed: [node2]

PLAY RECAP *************************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node2                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node3                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node4                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0

Everything looks good! Now let’s run a quick ad-hoc command to inspect the contents of the /tmp/server-info.txt file on one of the nodes:

[[email protected] plays]$ ansible node1 -m command -a "cat /tmp/server-info.txt"
node1 | CHANGED | rc=0 >>
Server Information Summary
--------------------------

hostname=node1
fqdn=node1.linuxhandbook.local
ipaddr=10.0.0.5
distro=CentOS
distro_version=8.2
nameservers=['168.63.129.16']
totalmem=1896
freemem=1087

As you can see, Jinja2 was able to access and process all the facts.

Conditional statements in Jinja2

You can use the if conditional statement in Jinja2 for testing various conditions and comparing variables. This allows you to determine your file template execution flow according to your test conditions.

To demonstrate, go to your templates directory and create the following selinux.j2 template:

[[email protected] templates]$ cat selinux.j2 
{% set selinux_status = ansible_facts['selinux']['status'] %}

{% if selinux_status == "enabled" %}
	"SELINUX IS ENABLED"
{% elif selinux_status == "disabled" %}
	"SELINUX IS DISABLED"
{% else %}
	"SELINUX IS NOT AVAILABLE"
{% endif %}

The first statement in the template creates a new variable selinux_statusand set its value to ansible_facts['selinux']['status'].

You then use selinux_status in your if test condition to determine whether SELinux is enabled, disabled, or not installed. In each of the three different cases, you display a message that reflects Selinux status.

Notice how the if statement in Jinja2 mimics Python’s if statement; just don’t forget to use {% endif %}.

Now go back to your project directory and create the following selinux-status.yml playbook:

[[email protected] plays]$ cat selinux-status.yml 
---
- name: Check SELinux Status
  hosts: all
  tasks:
    - name: Display SELinux Status
      debug:
        msg: "{{ ansible_facts['selinux']['status'] }}"

    - name: Create selinux.out using Jinja2
      template:
        src: selinux.j2
        dest: /tmp/selinux.out

Go ahead and run the playbook:

[[email protected] plays]$ ansible-playbook selinux-status.yml 

PLAY [Check SELinux Status] ****************************************************

TASK [Gathering Facts] *********************************************************
ok: [node4]
ok: [node2]
ok: [node3]
ok: [node1]

TASK [Display SELinux Status] **************************************************
ok: [node1] => {
    "msg": "enabled"
}
ok: [node2] => {
    "msg": "disabled"
}
ok: [node3] => {
    "msg": "enabled"
}
ok: [node4] => {
    "msg": "Missing selinux Python library"
}

TASK [Create selinux.out using Jinja2] *****************************************
changed: [node4]
changed: [node1]
changed: [node3]
changed: [node2]

PLAY RECAP *********************************************************************
node1                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    
node2                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    
node3                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    node4                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0  

From the playbook output; you can see that SELinux is enabled on both node1 and node3. I disabled SELinux on node2 before running the playbook and node4 doesn’t have SELinux installed because Ubuntu uses AppArmor instead of SELinux.

Finally, you can run the following ad-hoc command to inspect the contents of selinux.out on all the managed hosts:

[[email protected] plays]$ ansible all -m command -a "cat /tmp/selinux.out"
node4 | CHANGED | rc=0 >>

	"SELINUX IS NOT AVAILABLE"
 
node2 | CHANGED | rc=0 >>

	"SELINUX IS DISABLED"
 
node3 | CHANGED | rc=0 >>

	"SELINUX IS ENABLED"
 
node1 | CHANGED | rc=0 >>

	"SELINUX IS ENABLED"

Looping in Jinja2

You can use the for statement in Jinja2 to loop over items in a list, range, etc. For example, the following for loop will iterate over the numbers in the range(1,11)and will hence display the numbers from 1->10:

{% for i in range(1,11) %}
	Number {{ i }}
{% endfor %}

Notice how the for loop in Jinja2 mimics the syntax of Python’s for loop; again don’t forget to end the loop with {% endfor %}.

Now let’s create a full example that shows off the power of for loops in Jinja2. Change to your templates directory and create the following hosts.j2 template file:

[[email protected] templates]$ cat hosts.j2 
{% for host in groups['all'] %}
{{ hostvars[host].ansible_facts.default_ipv4.address }}  {{ hostvars[host].ansible_facts.fqdn }}  {{ hostvars[host].ansible_facts.hostname }}
{% endfor %}

Notice here you used a new built-in special (magic) variable hostvars which is basically a dictionary that contains all the hosts in inventory and variables assigned to them.

You iterated over all the hosts in your inventory and then for each host; you displayed the value of three variables:

  1. {{ hostvars[host].ansible_facts.default_ipv4.address }}
  2. {{ hostvars[host].ansible_facts.fqdn }}
  3. {{ hostvars[host].ansible_facts.hostname }}

Notice also that you must include those three variables on the same line side by side to match the format of the /etc/hosts file.

Now go back to your projects directory and create the following local-dns.yml playbook:

[[email protected] plays]$ cat local-dns.yml 
---
- name: Dynamically Update /etc/hosts File
  hosts: all
  tasks:
    - name: Update /etc/hosts using Jinja2
      template:
        src: hosts.j2
        dest: /etc/hosts

Then go ahead and run the playbook:

[[email protected] plays]$ ansible-playbook local-dns.yml 

PLAY [Dynamically Update /etc/hosts File] *********************************************

TASK [Gathering Facts] ***************************
ok: [node4]
ok: [node2]
ok: [node1]
ok: [node3]

TASK [Update /etc/hosts using Jinja2] ***********************************************
changed: [node4]
changed: [node3]
changed: [node1]
changed: [node2]

PLAY RECAP **********************
node1                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node2                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node3                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0    
node4                      : ok=2    changed=1    unreachable=0    failed=0    skipped=0 

Everything looks good so far; now run the following ad-hoc command to verify that /etc/hosts file is properly updated on node1:

[[email protected] plays]$ ansible node1 -m command -a "cat /etc/hosts"
node1 | CHANGED | rc=0 >>
10.0.0.5  node1.linuxhandbook.local  node1
10.0.0.6  node2.linuxhandbook.local  node2
10.0.0.7  node3.linuxhandbook.local  node3
10.0.0.8  node4.linuxhandbook.local  node4

Perfect! Looks properly formatted as you expected it to be.

I hope you now realize the power of Jinja2 templates in Ansible. Stay tuned for next tutorial where you will learn to protect sensitive information and files using Ansible Vault.



Join the conversation.