Jinja2 as Ansible's Expression Language
Every double-curly-brace expression in Ansible, such as {{ ansible_facts.hostname }} or {{ app_version }}, is evaluated by the Jinja2 templating engine, the same library used for YAML string interpolation and for rendering full configuration files with the template module. Jinja2 expressions can reference variables, call filters, perform arithmetic, and access nested data structures with dot or bracket notation, which is why {{ my_list[0] }} and {{ my_dict.key }} both work inside Ansible playbooks and templates.
Cricket analogy: A scorecard app fills in {{ batsman_name }} and {{ runs_scored }} placeholders in real time as commentary comes in, just as Jinja2 fills {{ }} expressions with live variable values when Ansible renders a playbook or template.
Filters: Transforming Values Inline
Filters, written with the pipe symbol like {{ my_var | default('none') }}, transform a value before it is used, and Ansible ships dozens of them beyond Jinja2's built-ins: to_json and from_yaml for serialization, regex_replace for pattern substitution, join and map for lists, and password_hash for generating crypt-style hashes. Filters can be chained, so {{ users | map(attribute='name') | join(', ') }} extracts every user's name from a list of dictionaries and joins them into one comma-separated string in a single expression.
Cricket analogy: A broadcast graphics pipeline pipes raw ball-tracking data through a 'convert to speed in km/h' filter, then a 'round to one decimal' filter, chaining transforms just like {{ speed | float | round(1) }}.
Conditionals and Loops Inside Templates
Beyond simple {{ }} substitution, Jinja2 supports control structures inside {% %} tags: {% if ansible_facts.os_family == 'Debian' %}...{% endif %} for conditional blocks, and {% for server in upstream_servers %}...{% endfor %} for generating repeated blocks like nginx upstream entries. Whitespace control markers, {%- and -%}, trim the newlines and indentation that Jinja2 would otherwise leave behind, which matters for generating clean config files like /etc/nginx/nginx.conf where stray blank lines can break strict parsers.
Cricket analogy: A DRS review system runs conditional logic — if the ball-tracking shows impact in line and hitting the stumps, show 'OUT', else show 'NOT OUT' — the same branching logic as {% if %}...{% else %}...{% endif %} in Jinja2.
The template Module vs Inline Templating
The ansible.builtin.template module renders a full .j2 file from the controller, substituting all Jinja2 expressions with resolved variable values, and copies the result to the target host — commonly used for nginx.conf.j2, my.cnf.j2, or systemd unit files. Unlike ansible.builtin.copy, template always processes the file through Jinja2 first, and it supports the same mode, owner, and validate parameters as copy, where validate lets you run a syntax check command like 'nginx -t -c %s' against the rendered file before it overwrites the live config.
Cricket analogy: A pitch curator prepares a base wicket according to a standard template but adjusts grass length for a day-night Test, then a groundsman inspector validates it before play, similar to template rendering a .j2 file and then validate checking the output.
# templates/nginx.conf.j2
user {{ nginx_user | default('www-data') }};
worker_processes {{ ansible_facts.processor_vcpus | default(1) }};
upstream backend {
{%- for server in upstream_servers %}
server {{ server.host }}:{{ server.port }};
{%- endfor %}
}
server {
listen {{ http_port | default(80) }};
{% if enable_ssl | default(false) %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
{% endif %}
server_name {{ inventory_hostname }};
}
# tasks/main.yml
- name: Deploy nginx config from template
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: 'nginx -t -c %s'
notify: reload nginxAdd {%- and -%} whitespace-control markers around {% for %} and {% if %} blocks when generating strict config file formats, so Jinja2 doesn't leave stray blank lines that some parsers (like older nginx or systemd unit files) reject.
Referencing an undefined variable inside a Jinja2 expression, like {{ typo_variable }}, raises an 'undefined variable' error and fails the task rather than silently rendering an empty string. Use the default filter, {{ my_var | default('fallback') }}, for any variable that might legitimately be unset.
- Every {{ }} expression in Ansible YAML and templates is evaluated by the Jinja2 templating engine.
- Filters, chained with the pipe symbol, transform values inline, e.g. {{ users | map(attribute='name') | join(', ') }}.
- {% if %} and {% for %} tags support conditionals and loops directly inside .j2 template files.
- Whitespace control markers {%- and -%} prevent stray blank lines in strictly-parsed config file formats.
- The template module always renders a .j2 file through Jinja2 before copying it to the target host.
- The validate parameter on the template module lets you syntax-check a rendered file before it overwrites a live config.
- Undefined variables raise a hard error in Jinja2 expressions; use the default filter to guard against them.
Practice what you learned
1. Which Ansible module always processes its source file through the Jinja2 engine before copying it to a host?
2. What does the filter chain {{ users | map(attribute='name') | join(', ') }} do?
3. What happens when a Jinja2 expression references a variable that is undefined?
4. What is the purpose of the validate parameter on the template module?
5. Why would you add {%- and -%} whitespace control markers to a {% for %} loop in a template?
Was this page helpful?
You May Also Like
Variables and Precedence
How Ansible collects variable values from more than twenty possible sources and resolves conflicts using a strict, well-defined precedence order.
Facts and Gathering Facts
How Ansible discovers system information automatically with the setup module, how to extend it with custom facts, and how fact caching speeds up large runs.
Group and Host Variables
How Ansible's group_vars and host_vars directories organize variables by inventory group and individual host, and how group nesting affects precedence.
Related Reading
Related Study Notes in DevOps
Browse all study notesNginx Study Notes
DevOps · 30 topics
DevOpsAdvanced Kubernetes Study Notes
Kubernetes · 30 topics
DevOpsAdvanced Bash Scripting Study Notes
Bash · 30 topics
DevOpsApache Kafka Study Notes
Kafka · 30 topics
DevOpsDocker & Kubernetes Study Notes
YAML · 40 topics
DevOpsCI/CD Tools & Pipelines Study Notes
YAML · 37 topics