Ansible Jinja2 Templating

Jinja2 is the templating engine that powers every dynamic value in Ansible — from variable substitution in playbooks to config file generation with the template module. Understanding Jinja2 well is what separates Ansible users who write static-feeling playbooks from those who write genuinely dynamic, data-driven automation. This lesson covers the Jinja2 features that appear most frequently in real-world Ansible work.

The Jinja2 Delimiter System

Jinja2 uses three types of delimiters:

  • {{ expression }} — Outputs the value of an expression (variable substitution, calculations, filter results)
  • {% statement %} — Control flow: if, for, set, block, macro
  • {# comment #} — Template comments (not rendered in the output)

In Ansible playbook YAML, variable substitution uses {{ }} everywhere. In template files (.j2), all three delimiters are available.

Variable Output and Expressions

{# Simple variable output #}
server_name {{ nginx_server_name }};

{# Arithmetic #}
worker_connections {{ nginx_worker_processes * 1024 }};

{# String concatenation #}
log_path {{ log_dir + '/' + app_name + '.log' }};

{# Ternary expression #}
listen {{ nginx_https_port if ssl_enabled else nginx_http_port }};

{# Default value if variable is undefined #}
timeout {{ request_timeout | default(30) }}s;

Filters: Transforming Data

Filters transform the value of an expression using the pipe | syntax. Ansible inherits Jinja2's built-in filters and adds many of its own:

{# String manipulation #}
{{ app_name | upper }}           # MYAPP
{{ app_name | lower }}           # myapp
{{ app_name | capitalize }}      # Myapp
{{ hostname | replace('-', '_') }}  # Replace characters

{# Type conversion #}
{{ "42" | int }}                 # 42 (integer)
{{ 3.7 | round }}                # 4.0
{{ enabled | bool }}             # Convert to boolean

{# List and dict operations #}
{{ packages | join(', ') }}      # nginx, curl, vim
{{ users | length }}             # Count items
{{ servers | sort }}             # Sort a list
{{ servers | unique }}           # Remove duplicates
{{ config | dict2items }}        # Convert dict to list of {key, value} items

{# Default and existence checks #}
{{ variable | default('fallback') }}
{{ variable | default(omit) }}   # Omit the argument entirely if undefined

{# Path manipulation #}
{{ '/etc/nginx/nginx.conf' | basename }}   # nginx.conf
{{ '/etc/nginx/nginx.conf' | dirname }}    # /etc/nginx

{# Cryptographic #}
{{ 'mypassword' | password_hash('sha512') }}

{# JSON and YAML #}
{{ data_structure | to_json }}
{{ data_structure | to_nice_json(indent=2) }}
{{ data_structure | to_yaml }}

Conditionals in Templates

{% if ssl_enabled %}
listen {{ nginx_https_port }} ssl;
ssl_certificate     {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% else %}
listen {{ nginx_http_port }};
{% endif %}

{% if ansible_os_family == "Debian" %}
include /etc/nginx/sites-enabled/*;
{% elif ansible_os_family == "RedHat" %}
include /etc/nginx/conf.d/*.conf;
{% endif %}

Loops in Templates

{% for server in nginx_upstream_servers %}
    server {{ server.host }}:{{ server.port }} weight={{ server.weight | default(1) }};
{% endfor %}

{# With loop index #}
{% for vhost in nginx_vhosts %}
# Virtual host {{ loop.index }} of {{ nginx_vhosts | length }}
server {
    server_name {{ vhost.server_name }};
    root {{ vhost.document_root }};
}
{% endfor %}

{# Loop with condition #}
{% for user in users if user.active %}
    {{ user.name }} ALL=(ALL) NOPASSWD:ALL
{% endfor %}

The set Statement

{% set config_file = nginx_conf_dir + '/' + app_name + '.conf' %}
# Config file location: {{ config_file }}

{% set worker_count = ansible_processor_vcpus | default(2) %}
worker_processes {{ worker_count }};

Whitespace Control

Jinja2 preserves whitespace by default, which can produce extra blank lines in generated files. Use the dash modifier to strip whitespace:

{%- if ssl_enabled %}    {# Strip whitespace before the tag #}
ssl on;
{%- endif %}             {# Strip whitespace after the tag #}

Macros: Reusable Template Fragments

{% macro upstream_block(name, servers) %}
upstream {{ name }} {
    {% for server in servers %}
    server {{ server }};
    {% endfor %}
}
{% endmacro %}

{{ upstream_block('backend', ['10.0.1.10:8080', '10.0.1.11:8080', '10.0.1.12:8080']) }}

Macros are reusable template functions — ideal for repeating structural patterns like upstream blocks, virtual host stubs, or ACL definitions.

Testing Jinja2 Expressions

Use the debug module in a playbook to test Jinja2 expressions without creating template files:

- name: Test Jinja2 filter
  debug:
    msg: "{{ packages | join(', ') | upper }}"

- name: Test conditional expression
  debug:
    msg: "{{ 'SSL enabled' if ssl_enabled else 'HTTP only' }}"

This tight feedback loop accelerates Jinja2 learning significantly. Test expressions interactively before embedding them in templates.

Try This: Build a Dynamic Config Generator

Create a Jinja2 template for an application config file that uses at least: two conditional blocks (e.g., SSL and debug mode), one loop (e.g., allowed IP addresses or upstream servers), three filters (e.g., default, upper, join), and one set statement. Generate the template with different variable values and confirm the output changes correctly. This exercise builds the full range of Jinja2 skills used in professional Ansible template work.

Summary

Jinja2 provides expression output with {{ }}, control flow with {% %}, and comments with {# #}. Filters transform data inline using the pipe syntax — essential for string manipulation, type conversion, list operations, and cryptographic functions. Conditionals and loops in templates generate config file sections dynamically based on variables. The set statement and macros enable more sophisticated template logic. Testing expressions with the debug module accelerates development.

Leave a Comment