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
- Use .j2 Extension: Name template files with .j2 suffix for clarity
- Add Headers: Include generation timestamp and warning in templates
- Validate Input: Use mandatory filter for required variables
- Default Values: Provide sensible defaults with the default filter
- Comment Complex Logic: Explain non-obvious template code
- Test Templates: Validate with check mode before deployment
- Keep It Simple: Move complex logic to playbooks, not templates
- 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
- Explore Playbooks to use templates in automation
- Learn about Roles for organizing templates
- Master Best Practices for template management
- Try the Playground to experiment with templates