Jinja2 templating is one of Ansible’s most powerful features, allowing you to create dynamic configuration files that adapt to different environments, servers, and conditions. This comprehensive guide covers everything from basic variable substitution to advanced template techniques with real-world examples you can use in production.
📑 Table of Contents
- What is Jinja2 Templating in Ansible?
- Why Use Templates?
- Basic Template Syntax
- Variable Substitution
- Accessing Ansible Facts
- Conditionals in Templates
- Basic If/Else
- If/Elif/Else
- Inline Conditionals (Ternary)
- Loops in Templates
- Simple For Loop
- Loop Over List of Dictionaries
- Loop Variables
- Conditional Loops
- Complete Template Example: Nginx Load Balancer
- Filters in Templates
- String Filters
- List Filters
- Math Filters
- Type Conversion Filters
- Complete Application Configuration Example
- Using Templates in Playbooks
- Basic Template Task
- Template with Validation
- Template with Variables
- Advanced Template Techniques
- Macros (Reusable Template Blocks)
- Whitespace Control
- Comments in Templates
- Including Other Templates
- Real-World Example: System Report Generator
- Server Details
- Network Interfaces
- Disk Usage
- Best Practices
- 1. Use ansible_managed Comment
- 2. Validate Templates Before Deployment
- 3. Use Backup Option
- 4. Set Proper Permissions
- 5. Use Default Filters
- 6. Keep Templates Simple
- Troubleshooting
- Issue: Undefined Variable Error
- Issue: Template Syntax Error
- Issue: Whitespace Issues
- Summary
What is Jinja2 Templating in Ansible?
Jinja2 is a modern templating engine for Python that Ansible uses to generate dynamic content. Instead of maintaining separate configuration files for each server or environment, you create one template that automatically adapts based on variables and conditions.
Why Use Templates?
- Dynamic Configuration: Generate configs based on server facts (CPU, memory, IP address)
- Environment-Specific: Same template works for dev, staging, and production
- Reduced Duplication: One template instead of dozens of static files
- Maintainability: Update one template instead of hunting through multiple files
- Validation: Test template syntax before deployment
Basic Template Syntax
Variable Substitution
The most basic Jinja2 operation is variable substitution using double curly braces:
# Template file: app.conf.j2
server_name = {{ server_name }}
listen_port = {{ app_port }}
environment = {{ environment }}
When Ansible processes this template with variables:
vars:
server_name: web01.example.com
app_port: 8080
environment: production
It generates:
server_name = web01.example.com
listen_port = 8080
environment = production
Accessing Ansible Facts
Ansible facts are system information automatically collected from target hosts. You can use these in templates:
# Template using facts
hostname = {{ ansible_hostname }}
ip_address = {{ ansible_default_ipv4.address }}
os = {{ ansible_distribution }} {{ ansible_distribution_version }}
cpu_cores = {{ ansible_processor_vcpus }}
total_memory = {{ ansible_memtotal_mb }}MB
Output on Ubuntu 20.04 server:
hostname = web01
ip_address = 192.168.1.10
os = Ubuntu 20.04
cpu_cores = 4
total_memory = 8192MB
Conditionals in Templates
Use {% if %}
statements to include content conditionally:
Basic If/Else
# Template: database.conf.j2
[database]
host = {{ db_host }}
port = {{ db_port }}
{% if environment == 'production' %}
# Production settings
max_connections = 200
pool_size = 50
ssl_mode = require
{% else %}
# Development settings
max_connections = 20
pool_size = 5
ssl_mode = prefer
{% endif %}
If/Elif/Else
# Logging configuration based on environment
[logging]
{% if environment == 'production' %}
level = WARNING
debug = false
log_file = /var/log/app/production.log
{% elif environment == 'staging' %}
level = INFO
debug = false
log_file = /var/log/app/staging.log
{% else %}
level = DEBUG
debug = true
log_file = /var/log/app/development.log
{% endif %}
Inline Conditionals (Ternary)
For simple conditions, use inline ternary expressions:
cache_enabled = {{ 'true' if environment == 'production' else 'false' }}
debug_mode = {{ 'false' if environment == 'production' else 'true' }}
ssl_enabled = {{ 'yes' if ssl_certificate is defined else 'no' }}
Loops in Templates
Jinja2 loops allow you to iterate over lists and dictionaries to generate repetitive configuration blocks.
Simple For Loop
# Generate server list
{% for server in app_servers %}
server {{ server }}
{% endfor %}
With variables:
vars:
app_servers:
- web01.example.com
- web02.example.com
- web03.example.com
Generates:
server web01.example.com
server web02.example.com
server web03.example.com
Loop Over List of Dictionaries
# Nginx upstream configuration
upstream backend {
{% for server in app_servers %}
server {{ server.ip }}:{{ server.port }} weight={{ server.weight }};
{% endfor %}
}
Variables:
vars:
app_servers:
- ip: 192.168.1.10
port: 8080
weight: 5
- ip: 192.168.1.11
port: 8080
weight: 3
- ip: 192.168.1.12
port: 8080
weight: 2
Result:
upstream backend {
server 192.168.1.10:8080 weight=5;
server 192.168.1.11:8080 weight=3;
server 192.168.1.12:8080 weight=2;
}
Loop Variables
Jinja2 provides special variables inside loops:
{% for user in users %}
User {{ loop.index }}: {{ user.name }}
{% if loop.first %}(First user){% endif %}
{% if loop.last %}(Last user){% endif %}
Remaining: {{ loop.revindex }}
{% endfor %}
Loop variables available:
loop.index
– Current iteration (1-indexed)loop.index0
– Current iteration (0-indexed)loop.first
– True on first iterationloop.last
– True on last iterationloop.length
– Total number of itemsloop.revindex
– Iterations remaining (1-indexed)
Conditional Loops
Filter items in loops using conditions:
# Only active users
{% for user in users if user.active %}
{{ user.name }} ({{ user.email }})
{% endfor %}
# Only admin users
{% for user in users if user.role == 'admin' %}
Admin: {{ user.name }}
{% endfor %}
Complete Template Example: Nginx Load Balancer
Let’s create a comprehensive Nginx load balancer configuration template:
# Template: nginx_lb.conf.j2
# {{ ansible_managed }}
# Generated on {{ ansible_date_time.iso8601 }}
upstream {{ app_name }}_backend {
{% for server in app_servers %}
server {{ server.ip }}:{{ server.port }} weight={{ server.weight }} max_fails=3 fail_timeout=30s;
{% endfor %}
keepalive 32;
}
server {
listen 80;
server_name {{ ansible_fqdn }};
access_log /var/log/nginx/{{ app_name }}_access.log;
error_log /var/log/nginx/{{ app_name }}_error.log;
location / {
proxy_pass http://{{ app_name }}_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
{% if environment == 'production' %}
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
{% else %}
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
{% endif %}
}
location /health {
access_log off;
return 200 "healthy
";
add_header Content-Type text/plain;
}
{% if environment == 'production' %}
# Production-only rate limiting
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
limit_req zone=one burst=20 nodelay;
{% endif %}
}
# Backend server summary
{% for server in app_servers %}
# {{ server.name }}: {{ server.ip }}:{{ server.port }} (weight: {{ server.weight }})
{% endfor %}
Playbook to use this template:
- name: Deploy Nginx Load Balancer
hosts: loadbalancers
become: yes
vars:
app_name: myapp
environment: production
app_servers:
- name: app1
ip: 192.168.1.10
port: 8080
weight: 5
- name: app2
ip: 192.168.1.11
port: 8080
weight: 3
tasks:
- name: Deploy Nginx configuration
template:
src: templates/nginx_lb.conf.j2
dest: /etc/nginx/sites-available/{{ app_name }}.conf
mode: '0644'
validate: 'nginx -t -c %s'
notify: Reload Nginx
handlers:
- name: Reload Nginx
service:
name: nginx
state: reloaded
Filters in Templates
Filters transform variables before outputting them. Syntax: {{ variable | filter }}
String Filters
# Convert case
{{ app_name | upper }} # MYAPP
{{ app_name | lower }} # myapp
{{ app_name | capitalize }} # Myapp
{{ app_name | title }} # My App
# String manipulation
{{ description | truncate(50) }} # Truncate to 50 chars
{{ name | replace('old', 'new') }} # Replace text
{{ path | basename }} # Get filename from path
{{ string | trim }} # Remove whitespace
# Default values
{{ undefined_var | default('default_value') }}
{{ empty_string | default('fallback', true) }} # true = treat empty as undefined
List Filters
# List operations
{{ servers | length }} # Number of items
{{ servers | first }} # First item
{{ servers | last }} # Last item
{{ servers | join(', ') }} # Join with delimiter
{{ servers | sort }} # Sort list
{{ servers | reverse }} # Reverse order
{{ servers | unique }} # Remove duplicates
# Extract from list of dictionaries
{{ users | map(attribute='name') | list }}
{{ users | selectattr('active', 'equalto', true) | list }}
{{ users | rejectattr('admin') | list }}
Math Filters
# Arithmetic
{{ memory_mb / 1024 | round(2) }} # 8.00 GB
{{ price | round(0, 'ceil') }} # Round up
{{ value | abs }} # Absolute value
{{ number | int }} # Convert to integer
{{ value | float }} # Convert to float
# Memory conversions
{{ ansible_memtotal_mb | human_readable }} # 8.0 GB
Type Conversion Filters
# Convert types
{{ '42' | int }} # String to integer
{{ 3.14 | string }} # Number to string
{{ 'yes' | bool }} # String to boolean
{{ data | to_json }} # Dict to JSON
{{ data | to_nice_json }} # Pretty JSON
{{ data | to_yaml }} # Dict to YAML
Complete Application Configuration Example
# Template: app_config.j2
# {{ ansible_managed }}
# Configuration for {{ app_name }}
# Generated on {{ ansible_date_time.iso8601 }}
[application]
name = {{ app_name }}
version = {{ app_version }}
environment = {{ environment }}
[server]
hostname = {{ ansible_hostname }}
ip_address = {{ ansible_default_ipv4.address }}
os = {{ ansible_distribution }} {{ ansible_distribution_version }}
cpu_cores = {{ ansible_processor_vcpus }}
memory_gb = {{ (ansible_memtotal_mb / 1024) | round(1) }}
[database]
host = {{ database.host }}
port = {{ database.port }}
name = {{ database.name }}
user = {{ database.user }}
max_connections = {{ database.max_connections }}
pool_size = {{ (database.max_connections * 0.2) | int }}
[logging]
{% if environment == 'production' %}
level = WARNING
debug = false
log_file = /var/log/{{ app_name }}/production.log
max_size = 100MB
backup_count = 10
{% elif environment == 'staging' %}
level = INFO
debug = false
log_file = /var/log/{{ app_name }}/staging.log
max_size = 50MB
backup_count = 5
{% else %}
level = DEBUG
debug = true
log_file = /var/log/{{ app_name }}/development.log
max_size = 10MB
backup_count = 3
{% endif %}
[features]
cache_enabled = {{ 'true' if environment == 'production' else 'false' }}
debug_toolbar = {{ 'false' if environment == 'production' else 'true' }}
profiling = {{ 'disabled' if environment == 'production' else 'enabled' }}
[security]
ssl_enabled = {{ 'yes' if ssl_certificate is defined else 'no' }}
{% if ssl_certificate is defined %}
ssl_certificate = {{ ssl_certificate }}
ssl_key = {{ ssl_key }}
{% endif %}
[upstream_servers]
{% for server in app_servers %}
server_{{ loop.index }} = {{ server.ip }}:{{ server.port }} (weight: {{ server.weight }})
{% endfor %}
# System Information
# OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
# Kernel: {{ ansible_kernel }}
# Python: {{ ansible_python_version }}
# Architecture: {{ ansible_architecture }}
Using Templates in Playbooks
Basic Template Task
- name: Deploy application configuration
template:
src: templates/app_config.j2
dest: /etc/myapp/config.ini
owner: root
group: root
mode: '0644'
Template with Validation
- name: Deploy Nginx config with validation
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
mode: '0644'
validate: 'nginx -t -c %s' # Validate before deploying
backup: yes # Backup old file
notify: Reload Nginx
Template with Variables
- name: Deploy user-specific config
template:
src: user_profile.j2
dest: "/home/{{ item.username }}/.bashrc"
owner: "{{ item.username }}"
mode: '0644'
loop: "{{ users }}"
when: item.active
Advanced Template Techniques
Macros (Reusable Template Blocks)
# Define macro
{% macro render_server(server) %}
server {
listen {{ server.port }};
server_name {{ server.name }};
root {{ server.document_root }};
}
{% endmacro %}
# Use macro
{% for server in vhosts %}
{{ render_server(server) }}
{% endfor %}
Whitespace Control
# Remove whitespace before
{%- if condition %}
content
{% endif %}
# Remove whitespace after
{% if condition -%}
content
{% endif %}
# Remove both
{%- if condition -%}
content
{%- endif -%}
Comments in Templates
{# This is a Jinja2 comment - won't appear in output #}
{#
Multi-line
comment
#}
# This is a regular comment in the output file
Including Other Templates
# Include another template
{% include 'header.j2' %}
# Main content here
{% include 'footer.j2' %}
Real-World Example: System Report Generator
# Template: system_report.html.j2
System Report - {{ ansible_hostname }}
System Information Report
Generated: {{ ansible_date_time.iso8601 }}
Server Details
Property Value
Hostname {{ ansible_hostname }}
FQDN {{ ansible_fqdn }}
OS {{ ansible_distribution }} {{ ansible_distribution_version }}
Kernel {{ ansible_kernel }}
CPU Cores {{ ansible_processor_vcpus }}
Memory {{ (ansible_memtotal_mb / 1024) | round(2) }} GB
Network Interfaces
Interface IP Address MAC Address
{% for interface in ansible_interfaces %}
{% if interface != 'lo' %}
{{ interface }}
{{ ansible_facts[interface]['ipv4']['address'] | default('N/A') }}
{{ ansible_facts[interface]['macaddress'] | default('N/A') }}
{% endif %}
{% endfor %}
Disk Usage
Mount Total Used Available Use %
{% for mount in ansible_mounts %}
{{ mount.mount }}
{{ (mount.size_total / 1024 / 1024 / 1024) | round(2) }} GB
{{ ((mount.size_total - mount.size_available) / 1024 / 1024 / 1024) | round(2) }} GB
{{ (mount.size_available / 1024 / 1024 / 1024) | round(2) }} GB
{{ ((mount.size_total - mount.size_available) / mount.size_total * 100) | round(2) }}%
{% endfor %}
Best Practices
1. Use ansible_managed Comment
Always start templates with the {{ ansible_managed }}
variable:
# {{ ansible_managed }}
# This file is managed by Ansible - DO NOT EDIT MANUALLY
2. Validate Templates Before Deployment
- name: Deploy config with validation
template:
src: app.conf.j2
dest: /etc/app/app.conf
validate: '/usr/bin/app --check-config %s'
3. Use Backup Option
- name: Deploy critical config
template:
src: database.conf.j2
dest: /etc/database/config.conf
backup: yes # Creates timestamped backup
4. Set Proper Permissions
- name: Deploy secure config
template:
src: secrets.conf.j2
dest: /etc/app/secrets.conf
mode: '0600' # Read/write for owner only
owner: appuser
group: appgroup
5. Use Default Filters
Protect against undefined variables:
database_host = {{ db_host | default('localhost') }}
cache_ttl = {{ cache_ttl | default(3600) }}
admin_email = {{ admin_email | default('admin@example.com') }}
6. Keep Templates Simple
Complex logic belongs in playbooks, not templates:
# Good - Simple template
server_name = {{ server_name }}
# Bad - Complex logic in template
{% if ansible_distribution == "Ubuntu" and ansible_distribution_major_version >= "20" %}
{% if environment == "production" and ssl_enabled %}
# Complex nested logic...
{% endif %}
{% endif %}
Troubleshooting
Issue: Undefined Variable Error
# Error: Variable 'db_host' is undefined
# Solution 1: Use default filter
database_host = {{ db_host | default('localhost') }}
# Solution 2: Check if defined
{% if db_host is defined %}
database_host = {{ db_host }}
{% endif %}
Issue: Template Syntax Error
# Validate template syntax before running playbook
ansible-playbook playbook.yml --syntax-check
# Check specific template
ansible all -m template -a "src=template.j2 dest=/tmp/test.conf" --check
Issue: Whitespace Issues
# Control whitespace with - modifier
{%- for item in list -%}
{{ item }}
{%- endfor -%}
Summary
Jinja2 templating in Ansible provides powerful dynamic configuration management:
- Use
{{ variable }}
for substitution - Use
{% if %}
for conditionals - Use
{% for %}
for loops - Apply filters with
{{ var | filter }}
- Access facts with
{{ ansible_* }}
- Always validate templates before deployment
- Keep templates simple and readable
- Use default filters for undefined variables
Templates eliminate configuration duplication and adapt automatically to different environments, making your infrastructure code more maintainable and less error-prone.
Was this article helpful?