Jinja2 Foundations

Page 07 — Template language used by Ansible. Variables, conditionals, loops, and filters.

What Jinja2 is

Jinja2 is a template language. It lets you build text files dynamically using variables and logic. Ansible uses it extensively in the template module to render config files before deploying them to hosts.

A Jinja2 template is a plain text file (typically ending in .j2) containing a mix of static text and Jinja2 expressions.

Why it matters

Instead of writing 10 nearly identical config files by hand, you write one template and feed in different variables for each host or environment. The template renders the correct file for each target.

Variable output

Use double curly braces to output a variable's value:

{{ nginx_port }}
{{ inventory_hostname }}
{{ ansible_default_ipv4.address }}

Anything inside {{ }} is evaluated and replaced with its value when the template renders.

Conditionals

{% if enable_tls %}
listen 443 ssl;
{% else %}
listen 80;
{% endif %}

What this means: if enable_tls is true, use the HTTPS listener; otherwise use plain HTTP.

Remember: Every {% if %} must have a matching {% endif %}. Missing it is one of the most common Jinja2 errors.

Loops

{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}

This renders one server ... iburst line for each item in the ntp_servers list.

Filters

Filters transform values. Apply them with a pipe character |:

{{ username | lower }}
{{ value | default('none') }}
{{ items | join(', ') }}
{{ path | basename }}
{{ number | int }}

Common filters:

Important difference: YAML vs Jinja2

People often confuse the two because Ansible uses both in the same project. YAML is for playbooks, inventory, and variable files. Jinja2 is for template files and inside quoted strings in tasks.

Example template

An nginx config template (nginx.conf.j2):

user nginx;
worker_processes auto;

events {
  worker_connections 1024;
}

http {
  server {
    listen {{ nginx_port }};
    server_name {{ server_name }};

    {% if enable_tls %}
    ssl_certificate     /etc/ssl/certs/{{ server_name }}.crt;
    ssl_certificate_key /etc/ssl/private/{{ server_name }}.key;
    {% endif %}
  }
}

Deployed with an Ansible task:

- name: Deploy nginx config
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  notify: Restart nginx

Common mistakes

Good habits

Ansible magic variables

Ansible automatically provides a set of special variables in every template and task. You do not define them — Ansible populates them from the inventory and runtime context.

Variable What it contains
inventory_hostname The name of the current host as written in your inventory
ansible_default_ipv4.address The primary IPv4 address of the current host
groups['groupname'] List of all hosts in the named inventory group
hostvars['hostname'] All Ansible facts and variables for any other host

Example: render a config line for every host in a group, using their IP addresses:

# In a Jinja2 template (.j2 file)
# Builds an allow-list of all backend server IPs

{% for host in groups['backends'] %}
allow {{ hostvars[host]['ansible_default_ipv4']['address'] }};
{% endfor %}

This loops over every host in the backends inventory group and writes an allow line per host using that host's primary IP. Ansible collects the IPs automatically during the play via fact-gathering.

Tip: Fact gathering must be enabled (the default) for ansible_default_ipv4 to be populated. If you turned off gathering with gather_facts: false, use hostvars[host]['ansible_host'] instead (the inventory address).

Jinja2 tests (is defined, is none…)

Jinja2 tests are used with the is keyword to check a value's state or type. They are cleaner and more explicit than relying on truthiness checks.

# is defined — the most useful test in Ansible templates
{% if db_password is defined %}
password={{ db_password }}
{% endif %}

# is none — check explicitly for None/null (different from undefined!)
{% if proxy_url is none %}
# no proxy configured
{% else %}
proxy={{ proxy_url }}
{% endif %}

# is string / is number / is iterable
{% if listen_ports is iterable and listen_ports is not string %}
{% for port in listen_ports %}
listen {{ port }};
{% endfor %}
{% else %}
listen {{ listen_ports }};
{% endif %}

# is mapping (dict check)
{% if extra_headers is mapping %}
{% for key, value in extra_headers.items() %}
add_header {{ key }} "{{ value }}";
{% endfor %}
{% endif %}
TestTrue when…
is definedVariable exists (not undefined)
is undefinedVariable does not exist
is noneValue is explicitly null
is stringValue is a string
is numberValue is int or float
is iterableValue can be iterated (list, dict, string)
is mappingValue is a dict
is sequenceValue is a list or string

Key Ansible filters

Ansible adds many filters on top of standard Jinja2. These are the ones you will see most often in real infrastructure templates.

ternary — inline if/else

# value | ternary(true_val, false_val)
listen_ssl: "{{ (enable_tls | bool) | ternary('443', '80') }}"
max_workers: "{{ (env == 'production') | ternary(16, 4) }}"

regex_replace — transform strings

safe_hostname: "{{ inventory_hostname | regex_replace('\\.', '_') }}"
# web01.internal.example.com → web01_internal_example_com

quote — shell safety

# Always quote variables used in shell commands
- name: Run script with user input
  ansible.builtin.shell: "/usr/bin/process {{ user_input | quote }}"

selectattr / rejectattr — filter a list of dicts

# users is a list of dicts with 'name', 'state', 'groups' keys
# Get only active users
active_users: "{{ users | selectattr('state', 'equalto', 'present') | list }}"

# Get users who are in the admin group
admins: "{{ users | selectattr('groups', 'contains', 'wheel') | list }}"

# Exclude disabled users
enabled: "{{ users | rejectattr('state', 'equalto', 'absent') | list }}"

dict2items / items2dict — convert between dict and list

# dict2items — iterate a dict as a list of {key, value} pairs
env_vars:
  APP_ENV: production
  LOG_LEVEL: warn
  DB_HOST: db01.internal

# In template:
{% for item in env_vars | dict2items %}
{{ item.key }}={{ item.value }}
{% endfor %}
# Renders:
# APP_ENV=production
# LOG_LEVEL=warn
# DB_HOST=db01.internal

# items2dict — rebuild a dict from a list
# Useful when group_vars has a list but you need key lookup
all_vars_dict: "{{ all_vars_list | items2dict(key_name='name', value_name='value') }}"

{% set %} template variables

Use {% set %} to create a local variable inside a template. This avoids repeating complex expressions and makes templates easier to read.

# Calculate a value once and reuse it
{% set max_conn = (ansible_memtotal_mb / 4) | int %}
max_connections = {{ max_conn }}
shared_buffers = {{ (max_conn * 8) }}MB

# Build a string conditionally
{% set ssl_dir = '/etc/pki/tls' if ansible_os_family == 'RedHat' else '/etc/ssl' %}
ssl_certificate {{ ssl_dir }}/certs/{{ inventory_hostname }}.crt;
ssl_certificate_key {{ ssl_dir }}/private/{{ inventory_hostname }}.key;

# Collect filtered items into a variable
{% set active_vhosts = vhosts | selectattr('enabled', 'equalto', true) | list %}
# Active vhosts: {{ active_vhosts | length }}
{% for vhost in active_vhosts %}
server_name {{ vhost.name }};
{% endfor %}

{% set %} variables are scoped to the current template. They do not persist between templates or back to the playbook — use set_fact for that.