100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
DevOps

Jinja2 Templating in Ansible

How Ansible uses the Jinja2 templating engine for variable substitution, filters, conditionals, and generating configuration files dynamically.

Variables & TemplatesIntermediate9 min readJul 10, 2026
Analogies

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.

jinja
# 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 nginx

Add {%- 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

Was this page helpful?

Topics covered

#DevOps#AnsibleStudyNotes#Jinja2TemplatingInAnsible#Jinja2#Templating#Ansible#Expression#StudyNotes#SkillVeris#ExamPrep