Jinja2 Templating in Ansible

What is Jinja2?

Jinja2 is a powerful templating engine that Ansible uses to enable dynamic expressions, variable access, and data transformation. All templating happens on the Ansible control node before tasks are sent to managed hosts.

Key Advantages:
  • Dynamic Content: Generate configuration files based on variables and facts
  • Control Node Processing: No Jinja2 installation needed on managed hosts
  • Rich Functionality: Filters, tests, loops, and conditionals
  • Reusability: Template once, deploy everywhere with different variables

Basic Templating Syntax

Variable Interpolation

# Simple variable
{{ variable_name }}

# Accessing nested data
{{ ansible_facts['hostname'] }}
{{ ansible_facts.distribution }}

# Dictionary access
{{ user['name'] }}
{{ user.email }}

# List access
{{ packages[0] }}
{{ servers | first }}

Creating Template Files

# templates/nginx.conf.j2
server {
    listen {{ nginx_port }};
    server_name {{ server_name }};
    root {{ document_root }};

    location / {
        proxy_pass {{ backend_url }};
    }
}

Using Templates in Playbooks

- name: Deploy nginx configuration
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/sites-available/{{ site_name }}
    owner: root
    group: root
    mode: '0644'
  notify: Reload nginx

Comments in Templates

{# This is a comment - won't appear in output #}

{#
Multi-line comment
for documentation
#}

# This is a literal comment in the output

Control Structures

Conditionals

# templates/config.j2
{% if ansible_distribution == 'Ubuntu' %}
Package manager: apt
{% elif ansible_distribution == 'CentOS' %}
Package manager: yum
{% else %}
Package manager: unknown
{% endif %}

# Inline conditionals
Database: {{ 'PostgreSQL' if use_postgres else 'MySQL' }}

# Multiple conditions
{% if is_production and ssl_enabled %}
SSL Certificate: {{ ssl_cert_path }}
{% endif %}

Loops

# Iterating over lists
{% for server in web_servers %}
Server {{ loop.index }}: {{ server.name }} - {{ server.ip }}
{% endfor %}

# Iterating over dictionaries
{% for key, value in config_options.items() %}
{{ key }} = {{ value }}
{% endfor %}

# Loop with conditionals
{% for user in users if user.active %}
{{ user.name }}: {{ user.email }}
{% endfor %}

# Loop variables
{% for item in items %}
  Index: {{ loop.index }}        {# 1, 2, 3, ... #}
  Index0: {{ loop.index0 }}      {# 0, 1, 2, ... #}
  First: {{ loop.first }}        {# True on first iteration #}
  Last: {{ loop.last }}          {# True on last iteration #}
  Length: {{ loop.length }}      {# Total items #}
{% endfor %}

Filters

Filters transform data in templates. They use the pipe (|) operator.

String Filters

# Case conversion
{{ hostname | upper }}
{{ hostname | lower }}
{{ title | capitalize }}
{{ title | title }}

# String manipulation
{{ message | replace('old', 'new') }}
{{ text | trim }}
{{ items | join(', ') }}

# Default values
{{ undefined_var | default('fallback') }}
{{ undefined_var | default(omit) }}  # Omit parameter if undefined

# Mandatory values
{{ required_var | mandatory }}  # Fail if not defined

# Encoding
{{ password | b64encode }}
{{ encoded_data | b64decode }}
{{ url_param | urlencode }}

List Filters

# Getting specific items
{{ servers | first }}
{{ servers | last }}
{{ servers | random }}

# Set operations
{{ list1 | unique }}
{{ list1 | union(list2) }}
{{ list1 | intersect(list2) }}
{{ list1 | difference(list2) }}

# Sorting
{{ items | sort }}
{{ items | reverse }}
{{ numbers | min }}
{{ numbers | max }}
{{ items | shuffle }}

# Filtering
{{ servers | select('defined') | list }}
{{ servers | reject('equalto', 'localhost') | list }}
{{ servers | selectattr('active', 'equalto', true) | list }}

# Flattening
{{ nested_list | flatten }}
{{ deeply_nested | flatten(levels=2) }}

# Mapping
{{ servers | map(attribute='name') | list }}
{{ ports | map('int') | list }}

Dictionary Filters

# Combining dictionaries
{{ dict1 | combine(dict2) }}
{{ dict1 | combine(dict2, recursive=True) }}

# Converting formats
{{ my_dict | dict2items }}
# Result: [{'key': 'foo', 'value': 'bar'}, ...]

{{ my_list | items2dict }}
# Converts list of dicts back to dict

# Extracting values
{{ users | map(attribute='email') | list }}
{{ inventory | json_query('hosts[*].ip') }}

Type Conversion Filters

# Type casting
{{ port_string | int }}
{{ is_enabled | bool }}
{{ price | float }}
{{ items | list }}

# Type checking
{{ variable | type_debug }}  # Shows variable type

# Formatting
{{ data | to_json }}
{{ data | to_nice_json }}
{{ data | to_yaml }}
{{ data | to_nice_yaml }}
{{ data | from_json }}
{{ data | from_yaml }}

Date and Time Filters

# Current timestamp
{{ ansible_date_time.epoch }}
{{ ansible_date_time.iso8601 }}

# Formatting dates
{{ '%Y-%m-%d' | strftime }}
{{ timestamp | strftime('%Y-%m-%d %H:%M:%S') }}

# Date calculations (requires dateutil)
{{ (ansible_date_time.epoch | int) + 86400 }}  # Add 1 day

Path Filters

# Path manipulation
{{ file_path | basename }}
{{ file_path | dirname }}
{{ file_path | expanduser }}
{{ file_path | realpath }}

# Examples
{{ '/etc/nginx/nginx.conf' | basename }}     # nginx.conf
{{ '/etc/nginx/nginx.conf' | dirname }}      # /etc/nginx
{{ '~/config.yml' | expanduser }}            # /home/user/config.yml

Hashing and Encryption Filters

# Hashing
{{ 'password' | hash('sha256') }}
{{ 'password' | hash('md5') }}
{{ 'data' | checksum }}

# Password hashing
{{ 'password' | password_hash('sha512') }}
{{ 'password' | password_hash('sha512', salt='mysalt') }}

# UUID generation
{{ ansible_hostname | to_uuid }}

IP Address Filters

# IP address validation and manipulation
{{ ip_address | ipaddr }}
{{ ip_address | ipv4 }}
{{ ip_address | ipv6 }}
{{ cidr | ipaddr('network') }}
{{ cidr | ipaddr('broadcast') }}
{{ cidr | ipaddr('netmask') }}
{{ cidr | ipaddr('host') }}

# Examples
{{ '192.168.1.10/24' | ipaddr('network') }}  # 192.168.1.0
{{ '192.168.1.10/24' | ipaddr('netmask') }}  # 255.255.255.0

Regex Filters

# Pattern matching
{{ string | regex_search('pattern') }}
{{ string | regex_findall('pattern') }}
{{ string | regex_replace('pattern', 'replacement') }}

# Examples
{{ 'IP: 192.168.1.1' | regex_search('\\d+\\.\\d+\\.\\d+\\.\\d+') }}
# Result: 192.168.1.1

{{ text | regex_replace('^# ', '') }}  # Remove leading '# '
{{ email | regex_replace('@.*', '@company.com') }}

JSON Query Filter (JMESPath)

# Complex data extraction
{{ servers | json_query('[?active].{name: name, ip: ip}') }}
{{ users | json_query('[].email') }}
{{ inventory | json_query('hosts[0].vars.ansible_host') }}

# Example data
servers:
  - name: web01
    ip: 192.168.1.10
    active: true
  - name: web02
    ip: 192.168.1.11
    active: false

# Query for active servers
{{ servers | json_query('[?active].name') }}
# Result: ['web01']

Tests

Tests check conditions and return true or false. They are used with `is` and `is not`.

Existence Tests

{% if variable is defined %}
Variable is defined
{% endif %}

{% if variable is undefined %}
Variable is not defined
{% endif %}

{% if variable is none %}
Variable is None
{% endif %}

Type Tests

{% if value is string %}
{% if value is number %}
{% if value is boolean %}
{% if value is mapping %}  {# dictionary #}
{% if value is sequence %}  {# list #}
{% if value is iterable %}

Comparison Tests

{% if ansible_distribution is equalto('Ubuntu') %}
{% if version is version('2.0', '>=') %}
{% if item is in list %}
{% if result is success %}
{% if result is failed %}
{% if result is changed %}
{% if result is skipped %}

String Tests

{% if hostname is match('^web.*') %}  {# Regex match from start #}
{% if hostname is search('web') %}    {# Regex search anywhere #}
{% if path is abs %}                  {# Absolute path #}
{% if file is file %}                 {# Is a file #}
{% if dir is directory %}             {# Is a directory #}
{% if path is exists %}               {# Path exists #}

Custom Ansible Tests

# Task result tests
{% if task_result is succeeded %}
{% if task_result is failed %}
{% if task_result is changed %}
{% if task_result is skipped %}

# Network tests
{% if ip is ansible.utils.ipaddr %}
{% if ip is ansible.utils.ipv4 %}
{% if ip is ansible.utils.ipv6 %}

# Subset/superset tests
{% if list1 is subset(list2) %}
{% if list1 is superset(list2) %}

Advanced Templating Techniques

Macros (Reusable Template Blocks)

{# Define a macro #}
{% macro server_block(name, port, root) %}
server {
    server_name {{ name }};
    listen {{ port }};
    root {{ root }};
}
{% endmacro %}

{# Use the macro #}
{{ server_block('example.com', 80, '/var/www') }}
{{ server_block('api.example.com', 8080, '/var/www/api') }}

Template Inheritance

# templates/base.conf.j2
{# Base template #}
[general]
{% block general_section %}
hostname = {{ inventory_hostname }}
{% endblock %}

[custom]
{% block custom_section %}
{# Default content #}
{% endblock %}

# templates/app.conf.j2
{% extends "base.conf.j2" %}

{% block custom_section %}
app_name = {{ app_name }}
app_port = {{ app_port }}
{% endblock %}

Including Other Templates

# templates/main.conf.j2
[main]
{% include 'header.j2' %}

{% for server in servers %}
{% include 'server_block.j2' %}
{% endfor %}

{% include 'footer.j2' %}

Variable Scoping

# Set variables in template
{% set backup_dir = '/var/backups' %}
{% set timestamp = ansible_date_time.epoch %}

Backup location: {{ backup_dir }}/backup-{{ timestamp }}

# Namespace for avoiding conflicts
{% set ns = namespace(counter=0) %}
{% for item in items %}
  {% set ns.counter = ns.counter + 1 %}
  Item {{ ns.counter }}: {{ item }}
{% endfor %}

Whitespace Control

# Remove whitespace before/after blocks
{%- if condition %}   {# Remove whitespace before #}
content
{% endif -%}          {# Remove whitespace after #}

# Example
{%- for item in items %}
{{ item }}
{%- endfor %}
# No blank lines between items

Practical Examples

Example 1: Dynamic Hosts File

# templates/hosts.j2
127.0.0.1   localhost

# Application servers
{% for host in groups['webservers'] %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }}    {{ host }}
{% endfor %}

# Database servers
{% for host in groups['databases'] %}
{{ hostvars[host]['ansible_default_ipv4']['address'] }}    {{ host }}.db
{% endfor %}

Example 2: Complex Nginx Configuration

# templates/nginx-site.conf.j2
upstream {{ app_name }}_backend {
{% for server in backend_servers %}
    server {{ server.ip }}:{{ server.port }}{% if server.weight is defined %} weight={{ server.weight }}{% endif %};
{% endfor %}
}

server {
    listen {{ nginx_port }}{% if ssl_enabled %} ssl http2{% endif %};
    server_name {{ server_name }};

{% if ssl_enabled %}
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    ssl_protocols TLSv1.2 TLSv1.3;
{% endif %}

    root {{ document_root }};
    index index.html index.htm;

{% for location in locations %}
    location {{ location.path }} {
        {% if location.proxy_pass is defined %}
        proxy_pass {{ location.proxy_pass }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        {% else %}
        try_files $uri $uri/ =404;
        {% endif %}
    }
{% endfor %}
}

Example 3: Systemd Service File

# templates/app.service.j2
[Unit]
Description={{ app_description }}
After=network.target{% if requires_database %} postgresql.service{% endif %}

[Service]
Type={{ service_type | default('simple') }}
User={{ app_user }}
Group={{ app_group }}
WorkingDirectory={{ app_dir }}
ExecStart={{ app_exec_start }}
{% if app_exec_reload is defined %}
ExecReload={{ app_exec_reload }}
{% endif %}
Restart={{ restart_policy | default('on-failure') }}
RestartSec={{ restart_sec | default(5) }}

Environment="PATH={{ app_path }}"
{% for key, value in env_vars.items() %}
Environment="{{ key }}={{ value }}"
{% endfor %}

[Install]
WantedBy=multi-user.target

Example 4: Configuration with Conditionals

# templates/app-config.yml.j2
---
application:
  name: {{ app_name }}
  environment: {{ environment }}
  debug: {{ 'true' if environment == 'development' else 'false' }}

database:
  host: {{ db_host }}
  port: {{ db_port }}
  name: {{ db_name }}
{% if db_pool_size is defined %}
  pool_size: {{ db_pool_size }}
{% endif %}

{% if redis_enabled %}
cache:
  type: redis
  host: {{ redis_host }}
  port: {{ redis_port | default(6379) }}
{% endif %}

logging:
  level: {{ log_level | default('INFO') }}
  {% if environment == 'production' %}
  handlers:
    - type: file
      path: /var/log/{{ app_name }}/app.log
    - type: syslog
  {% else %}
  handlers:
    - type: console
  {% endif %}

Templating in Playbooks (Inline)

- name: Create configuration with inline template
  copy:
    content: |
      # Generated on {{ ansible_date_time.iso8601 }}
      [database]
      host={{ db_host }}
      port={{ db_port }}
    dest: /etc/app/config.ini

- name: Set fact with template
  set_fact:
    backup_path: "{{ backup_dir }}/{{ ansible_hostname }}-{{ ansible_date_time.date }}.tar.gz"

- name: Use template in command
  command: "mysqldump -h {{ db_host }} {{ db_name }} > {{ backup_path }}"

Best Practices

  1. Use .j2 Extension: Name template files with .j2 suffix for clarity
  2. Add Headers: Include generation timestamp and warning in templates
  3. Validate Input: Use mandatory filter for required variables
  4. Default Values: Provide sensible defaults with the default filter
  5. Comment Complex Logic: Explain non-obvious template code
  6. Test Templates: Validate with check mode before deployment
  7. Keep It Simple: Move complex logic to playbooks, not templates
  8. Use Macros: For repeated template patterns

Template Header Example

# {{ ansible_managed }}
#
# WARNING: This file is automatically generated by Ansible
# Template: {{ template_path | default('unknown') }}
# Generated: {{ ansible_date_time.iso8601 }}
#
# DO NOT EDIT THIS FILE MANUALLY - Your changes will be overwritten

Common Pitfalls

Avoid These Mistakes:
  • Undefined Variables: Always use | default() or | mandatory
  • Type Mismatches: Cast types explicitly (| int, | string)
  • UTF-8 Encoding: Ensure templates are UTF-8 encoded
  • Quoting: Quote variables in YAML when they start with {{ }}
  • Complex Logic: Don't put business logic in templates

Debugging Templates

# Check template rendering without deploying
- name: Test template locally
  template:
    src: config.j2
    dest: /tmp/test-config
  check_mode: yes
  diff: yes

# Debug template output
- name: Render template to variable
  set_fact:
    config_content: "{{ lookup('template', 'config.j2') }}"

- name: Show rendered template
  debug:
    var: config_content

# Validate template syntax
# Create test playbook
- hosts: localhost
  gather_facts: no
  vars:
    test_var: value
  tasks:
    - debug:
        msg: "{{ lookup('template', 'templates/test.j2') }}"

Quick Reference

# Variable interpolation
{{ variable }}

# Filters (transform data)
{{ variable | filter_name }}
{{ variable | filter1 | filter2 }}

# Tests (check conditions)
{% if variable is test_name %}

# Comments
{# This won't appear in output #}

# Control structures
{% if condition %}...{% endif %}
{% for item in list %}...{% endfor %}

# Default values
{{ var | default('fallback') }}
{{ var | mandatory }}

# Common filters
| upper, lower, capitalize
| replace('old', 'new')
| default('value')
| first, last, random
| unique, sort, reverse
| join(', ')
| to_json, to_yaml
| b64encode, b64decode

Next Steps