Ansible Curriculum
What Is Jinja2
Ansible's power comes not from running the same fixed command on every host, but from adapting what it does based on context. The tool that makes this adaptation possible is Jinja2 — a templating engine originally built for Python web frameworks and now deeply embedded into every corner of Ansible.
When Ansible renders a playbook task, a variable file, or a configuration template, it passes the content through Jinja2 before executing anything. Jinja2 scans for its special syntax markers and replaces them with real values from the variable and fact context. By the time Ansible actually runs a module, every {{ variable }} has been resolved, every {% if %} block has been evaluated, and every {% for %} loop has been expanded — leaving only the final static content behind.
Understanding Jinja2 is not optional if you want to write real Ansible automation. It is the difference between a playbook that does one thing for all hosts and a playbook that does the right thing for each host.
Where Jinja2 Is Used in Ansible
Jinja2 expressions appear in multiple places across an Ansible project:
Task parameters: Module arguments can contain variable references that Jinja2 resolves before the module runs.
Conditional expressions: The when directive is a Jinja2 expression, but without the outer {{ }} braces.
Loop items: When you use loop, each item is a Jinja2 context variable called item.
Template files: .j2 files sent to the template module contain full Jinja2 syntax — expressions, control structures, and filters — all of which get resolved before the file is written to the target host.
Variable files and group_vars: Variable files themselves can reference other variables using Jinja2. This allows you to build computed values like full URLs from component parts.
The Three Jinja2 Delimiter Types
All Jinja2 syntax uses one of three delimiter pairs. Each one has a distinct purpose and you need to keep them straight:
Expression delimiters {{ }} — Used to output a value. Whatever is inside gets evaluated and its string representation is inserted into the output.
Statement delimiters {% %} — Used for control flow: if/else blocks, for loops, set statements, macro definitions. These blocks do not produce output themselves — they control what other content gets rendered.
Comment delimiters {# #} — Used to write comments inside template files. These are stripped from the output entirely and are invisible in the rendered file.
How Jinja2 Renders Templates: The Execution Model
When Ansible processes any file containing Jinja2 syntax, it goes through a clear sequence:
- Load the source — Ansible reads the
.j2template file or the playbook content from the control node's filesystem. - Build the variable context — Ansible assembles the full variable dictionary for the current host: inventory variables,
group_vars,host_vars, registered variables, facts, extra vars, and role variables — all merged according to precedence rules. - Pass to the Jinja2 engine — The raw content and the variable context are handed to the Jinja2 library. Jinja2 processes the content left to right, top to bottom.
- Resolve expressions — Every
{{ expression }}is evaluated in the context of the host's variables. The result is converted to a string and inserted in place. - Execute control statements — Every
{% if %},{% for %}, and{% set %}block is evaluated. True branches are kept, false branches are discarded, loops are expanded. - Strip comments — All
{# comment #}blocks are removed from the output. - Produce the final output — The rendered, static content is what Ansible uses: it either writes it to a file on the target host or uses it as the value of a module parameter.
Why Jinja2 Makes Playbooks Reusable
The fundamental problem that Jinja2 solves is environment-specific configuration. Without templating, you would need a separate file for every environment: nginx.conf.staging, nginx.conf.production, nginx.conf.dev. Every time you changed the config logic, you would need to update all three files independently — a maintenance nightmare.
With Jinja2, you write one nginx.conf.j2 template. The differences between environments — the port number, the server name, the upstream backend IPs, the SSL certificate path — are all variables. At deployment time, Ansible fills in the correct values for each environment automatically.
This same principle applies to cross-platform automation. A single template for an application config file can handle both Debian and RedHat systems because the file paths, user names, and package locations are all templated variables rather than hardcoded values.
Whitespace Control in Templates
One detail that trips up new Ansible users is whitespace. By default, Jinja2 preserves all whitespace around block tags. If you write:
The rendered output will have blank lines where the {% %} statements were. To strip the whitespace before or after a tag, add a dash inside the delimiter:
This is important when generating configuration files where extra blank lines could cause parsing errors or look messy.
A Simple End-to-End Example
Here is a minimal but complete example showing a template, the variables it uses, and the rendered output:
Template file (templates/motd.j2):
Playbook task:
Rendered output on web-prod-01:
The template was written once. Every host in the inventory gets its own version of the file — with its own hostname, OS details, and environment values — without any manual customization.
Now that you understand what Jinja2 is and why it exists inside Ansible, the next step is to get fluent in the actual syntax: the full set of delimiters, expressions, operators, and built-in tests that the Jinja2 language provides.