Expert · Ansible in depth · topic 51 of 57 · 30 min
Jinja2 templates
Explanation
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
| Syntax | Meaning |
|---|---|
| {{ 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 |
{# 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 %}- 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
{# 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 %}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?
| Module | Best when |
|---|---|
| copy | The file is identical on every host (a fixed banner, a certificate) |
| lineinfile | You own only one or two lines of a file that a package also manages |
| template | You 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
$ 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 anythingCommon 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
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.
backend exam_web
balance roundrobin
{% for host in groups['webservers'] %}
server {{ host }} {{ hostvars[host]['ansible_facts']['default_ipv4']['address'] }}:8080 check
{% endfor %}Practice (0/3 done)
Which module renders Jinja2 placeholders in a file?
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.
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Roles, collections and ansible-galaxyAnsible in depth · same module
- Roles, templates, Galaxy and VaultDevOps: Git, Docker, Kubernetes and CI/CD · Configuration management with Ansible, and YAML
- Ansible playbooksDevOps: Git, Docker, Kubernetes and CI/CD · Configuration management with Ansible, and YAML
- Templates: code for any typeC++ Programming (modern C++ and DSA) · Templates, smart pointers and move semantics
- Strings and template literalsJavaScript Programming (language in depth) · Strings, decisions and loops
- Environment variables and secretsFull-Stack Development · Deploying and running the app
Questions people ask about this
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
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.
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.
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
- 2 more topics in “Ansible in depth” before its exam: Roles, collections and ansible-galaxy, RHEL System Roles and Ansible Vault.
- Module 10 exam: Ansible in depth · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Certifications and career. 57 topics left in the course before the final exam.