Skip to content

Expert · Ansible in depth · topic 51 of 57 · 30 min

Jinja2 templates

Explanation

A template is a config file with placeholders. Jinja2 fills them in per host: {{ variable }} prints a value, {% if %} and {% for %} add logic, and filters like | default, | upper and | join shape values. The template module renders a .j2 file and copies the result to the host, reporting changed only if the result differs.

Why templates?

Config files are mostly the same on every server but differ in a few places: the host name, the IP, the list of backend servers. copy can only send a fixed file. template sends a file built for each host from its variables and facts, so one .j2 file serves the whole fleet.

The building blocks

SyntaxMeaning
{{ expression }}Print a value
{% statement %}Logic: if, for, set
{# comment #}A comment that does not appear in the output
{{ value | filter }}Transform a value: default('x'), upper, lower, join(', '), int, length
output
{# templates/motd.j2 #}
{{ ansible_managed }}
Welcome to {{ inventory_hostname | upper }}
Running {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
Admin contact: {{ admin_email | default('ops@example.com') }}
{% if 'prod' in group_names %}
WARNING: this is a PRODUCTION server. All actions are logged.
{% endif %}
ansible_managed is a standard comment line ("Ansible managed") telling people not to edit the file by hand.
yaml
- name: Message of the day
  ansible.builtin.template:
    src: templates/motd.j2
    dest: /etc/motd
    owner: root
    group: root
    mode: "0644"

Loops in templates: a classic /etc/hosts

output
{# templates/hosts.j2 #}
127.0.0.1   localhost localhost.localdomain
::1         localhost localhost.localdomain
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }}  {{ hostvars[host]['ansible_facts']['fqdn'] }}  {{ hostvars[host]['ansible_facts']['hostname'] }}
{% endfor %}
hostvars gives each host access to the facts of every other host, as long as facts were gathered for them in this run.

Facts for other hosts exist only if those hosts were in a play that gathered facts in the same run. When the template runs on a subset (--limit), the hosts not included have no facts and the template fails with an undefined variable. Run the play on all hosts, or gather facts for all first.

Whitespace

Lines with {% %} tags leave empty lines in the output. Ansible sets trim_blocks for templates, which removes the newline after a block tag. You can also add a dash inside the tag ({%- or -%}) to strip whitespace on that side. If a generated file has odd blank lines, this is why.

copy, lineinfile or template?

ModuleBest when
copyThe file is identical on every host (a fixed banner, a certificate)
lineinfileYou own only one or two lines of a file that a package also manages
templateYou own the whole file and parts of it differ per host or environment

Mixing them on the same file causes fights: a template run rewrites the whole file and removes lines another task added with lineinfile. Pick one owner per file.

Testing a template quickly

bash
$ ansible web01.example.com -m ansible.builtin.debug -a "msg={{ inventory_hostname | upper }}"
web01.example.com | SUCCESS => {
    "msg": "WEB01.EXAMPLE.COM"
}
$ ansible-playbook motd.yml --check --diff --limit web01.example.com   # see the diff without changing anything

Common mistakes

  • Writing the template in the copy module: copy does not render Jinja. Use template.
  • Using a variable that is not defined for some hosts: use | default(...) or define it in group_vars/all.
  • Missing {% endif %} or {% endfor %}: the error says "Unexpected end of template".
  • Editing the generated file on the server; the next run overwrites it. Change the template instead.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Jinja2 templates · 1/6

one .j2 file → many hosts

A template is a config file with placeholders, filled in per host.

Prefer a video by a teacher? Find free videos on “ansible jinja2 template module tutorial hostvars for loop” on YouTube ↗

In real life: Load balancer config for an online exam platform

An online exam platform in Noida adds web servers before every big exam. Its HAProxy config is a template that loops over groups['webservers'] and writes one "server" line per host with its IP. Adding a server is now one line in the inventory; the playbook renders a new config, the template task reports changed, and a handler reloads HAProxy.

output
backend exam_web
    balance roundrobin
{% for host in groups['webservers'] %}
    server {{ host }} {{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }}:8080 check
{% endfor %}
Part of templates/haproxy.cfg.j2.

Practice (0/3 done)

Task 1 · Question

Which module renders Jinja2 placeholders in a file?

Task 2 · Code · Python

Write a tiny template renderer render(template, variables). Replace every {{ name }} (spaces inside the braces are optional) with str(variables[name]). Support two filters: "| upper" and "| default('text')" (used when the name is missing). A missing name without default becomes "". Use the re module.

Task 3 · Practice

Write templates/hosts.j2 that builds /etc/hosts from the facts of all hosts in the inventory, and a play that deploys it to the group dev only (but gathers facts from all hosts first). Check the result on one host.

Quick quiz

Question 1

1.Which syntax prints a value in Jinja2?

Question 2

2.What does {{ port | default(80) }} do when port is not defined?

Question 3

3.A hosts template using hostvars fails with an undefined variable when run with --limit web01. Why?

Question 4

4.What is ansible_managed used for?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which syntax prints a value in Jinja2?

Answer: {{ value }}

Double braces print; {% %} is logic; {# #} is a comment.

What does {{ port | default(80) }} do when port is not defined?

Answer: Prints 80

default supplies a fallback.

A hosts template using hostvars fails with an undefined variable when run with --limit web01. Why?

Answer: Facts for the other hosts were not gathered in this run

Gather facts for all hosts first.

What is ansible_managed used for?

Answer: A header saying the file is managed by Ansible

It warns people not to edit by hand.

Which module writes a file from a Jinja2 .j2 file with variables filled in?Related

Answer: template

template renders Jinja2.

From Everyday modules and handlers

What is a collection in Ansible?Related

Answer: A package of modules, roles and plugins under a namespace, like community.general

Collections are how modules are packaged and shared, for example through Galaxy.

From Roles, templates, Galaxy and Vault

Which magic variable holds the host's name as written in the inventory?Related

Answer: inventory_hostname

inventory_hostname; ansible_facts['hostname'] is what the host reports itself.

From Variables, facts and registered results

Two tasks notify "Reload nginx" and both report changed. How many times does the handler run on that host?Related

Answer: Once, at the end of the play

Handlers are deduplicated and run once, after the tasks of the play.

From Ansible playbooks

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Ansible in depth” before its exam: Roles, collections and ansible-galaxy, RHEL System Roles and Ansible Vault.
  2. Module 10 exam: Ansible in depth · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Certifications and career. 57 topics left in the course before the final exam.

← Previous: Loops, conditions and error handling