Templates & Jinja2
What are Templates? Templates in Ansible use the Jinja2 templating engine to create dynamic configuration files. They allow you to generate customized files for each host by combining static content with variables, conditionals, loops, and filters.
Understanding Templates
Why Use Templates?
Templates solve configuration management challenges:
- Dynamic Configuration: Generate host-specific config files
- DRY Principle: Don't repeat yourself across multiple files
- Environment Flexibility: Same template for dev/staging/production
- Variable Substitution: Insert inventory variables and facts
- Complex Logic: Conditionals and loops in configuration
Template Processing
Key concept: All templating happens on the Ansible control node before sending to target hosts. This minimizes dependencies on target systems.
Basic Template Module
Using the Template Module
---
- name: Deploy configuration file
hosts: webservers
tasks:
- name: Create nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: reload nginx
handlers:
- name: reload nginx
ansible.builtin.service:
name: nginx
state: reloaded
Template File Structure
Create template files with .j2 extension:
# templates/nginx.conf.j2
user {{ nginx_user }};
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
}
http {
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
server {
listen {{ http_port | default(80) }};
server_name {{ inventory_hostname }};
root /var/www/html;
location / {
index index.html;
}
}
}
Jinja2 Syntax Basics
Variable Interpolation
# Simple variable
{{ variable_name }}
# Dictionary access
{{ ansible_facts['hostname'] }}
{{ server_config.port }}
# List access
{{ allowed_hosts[0] }}
# With default value
{{ custom_var | default('default_value') }}
# Ansible facts
{{ ansible_distribution }}
{{ ansible_default_ipv4.address }}
Comments
{# This is a comment - not included in output #}
{#
Multi-line comment
Also not included in output
#}
# This is included in output (regular config comment)
Statements and Logic
{% if condition %}
# Content when condition is true
{% endif %}
{% for item in list %}
# Content repeated for each item
{% endfor %}
{% set variable_name = 'value' %}
Conditionals in Templates
If Statements
# templates/app.conf.j2
{% if environment == 'production' %}
log_level = WARNING
debug = False
{% else %}
log_level = DEBUG
debug = True
{% endif %}
# Multi-condition
{% if ansible_distribution == 'Ubuntu' and ansible_distribution_major_version == '22' %}
use_systemd = True
{% elif ansible_distribution == 'CentOS' %}
use_systemd = True
{% else %}
use_systemd = False
{% endif %}
# With defined check
{% if custom_setting is defined %}
custom_option = {{ custom_setting }}
{% endif %}
Inline Conditionals (Ternary)
# Ternary operator
max_connections = {{ (environment == 'production') | ternary(1000, 100) }}
# With default
port = {{ custom_port if custom_port is defined else 8080 }}
Loops in Templates
Basic For Loops
# templates/hosts.j2
# Loop through list
{% for host in webservers %}
{{ host.ip }} {{ host.name }}
{% endfor %}
# Loop through dictionary
{% for key, value in server_config.items() %}
{{ key }} = {{ value }}
{% endfor %}
# Loop through inventory group
{% for host in groups['databases'] %}
server {{ loop.index }} {{ hostvars[host]['ansible_host'] }}
{% endfor %}
Loop Variables
{% for item in items %}
{{ loop.index }} # Current iteration (1-indexed)
{{ loop.index0 }} # Current iteration (0-indexed)
{{ loop.first }} # True if first iteration
{{ loop.last }} # True if last iteration
{{ loop.length }} # Number of items
{{ loop.revindex }} # Reverse index (1-indexed)
{{ loop.revindex0 }} # Reverse index (0-indexed)
{% endfor %}
# Example: Comma-separated list without trailing comma
{% for server in servers %}
{{ server.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
Conditional Loops
# templates/app_servers.conf.j2
# Loop with filter
{% for host in groups['webservers'] if hostvars[host]['environment'] == 'production' %}
server {{ hostvars[host]['ansible_host'] }}:8080;
{% endfor %}
# Loop with else (when list is empty)
{% for user in admin_users %}
{{ user.name }}: {{ user.email }}
{% else %}
# No admin users configured
{% endfor %}
Jinja2 Filters
Common Filters
# String manipulation
{{ hostname | upper }} # HOSTNAME
{{ hostname | lower }} # hostname
{{ description | capitalize }} # Description
{{ text | replace('old', 'new') }} # Replace text
# Default values
{{ variable | default('default_value') }}
{{ variable | default(omit) }} # Omit parameter if undefined
# Type conversion
{{ port | int }} # Convert to integer
{{ enabled | bool }} # Convert to boolean
{{ items | list }} # Convert to list
{{ data | string }} # Convert to string
# Lists and filtering
{{ [1, 2, 3, 4] | first }} # 1
{{ [1, 2, 3, 4] | last }} # 4
{{ [1, 2, 3, 4] | length }} # 4
{{ [1, 2, 3, 4] | min }} # 1
{{ [1, 2, 3, 4] | max }} # 4
{{ [1, 2, 3, 4] | sum }} # 10
{{ [1, 2, 3] | join(',') }} # "1,2,3"
# Unique and sorting
{{ [1, 2, 2, 3] | unique }} # [1, 2, 3]
{{ [3, 1, 2] | sort }} # [1, 2, 3]
{{ [1, 2, 3] | reverse }} # [3, 2, 1]
# Dictionary operations
{{ {'a': 1, 'b': 2} | dict2items }}
{{ [{'key': 'a', 'value': 1}] | items2dict }}
Ansible-Specific Filters
# IP address filters
{{ ansible_default_ipv4.address | ipaddr }}
{{ '192.168.1.0/24' | ipaddr('network') }}
{{ '192.168.1.0/24' | ipaddr('broadcast') }}
# Password hashing
{{ 'password123' | password_hash('sha512') }}
# File paths
{{ '/path/to/file.txt' | basename }} # file.txt
{{ '/path/to/file.txt' | dirname }} # /path/to
# JSON/YAML
{{ variable | to_json }}
{{ variable | to_nice_json }}
{{ variable | to_yaml }}
{{ variable | from_json }}
{{ variable | from_yaml }}
# Regular expressions
{{ 'test string' | regex_search('test') }}
{{ 'test123' | regex_replace('[0-9]+', 'NUM') }} # testNUM
Common Template Patterns
Apache Virtual Host Template
# templates/vhost.conf.j2ServerName {{ server_name }} {% if server_aliases is defined %} ServerAlias {{ server_aliases | join(' ') }} {% endif %} DocumentRoot {{ document_root }} Options -Indexes +FollowSymLinks AllowOverride All Require all granted {% if enable_ssl | default(false) %} SSLEngine on SSLCertificateFile {{ ssl_cert_file }} SSLCertificateKeyFile {{ ssl_key_file }} {% endif %} ErrorLog ${APACHE_LOG_DIR}/{{ server_name }}-error.log CustomLog ${APACHE_LOG_DIR}/{{ server_name }}-access.log combined {% if custom_directives is defined %} # Custom directives {% for directive in custom_directives %} {{ directive }} {% endfor %} {% endif %}
HAProxy Configuration Template
# templates/haproxy.cfg.j2
global
log /dev/log local0
maxconn {{ haproxy_maxconn | default(4096) }}
user haproxy
group haproxy
daemon
defaults
log global
mode http
option httplog
option dontlognull
timeout connect {{ timeout_connect | default('5000ms') }}
timeout client {{ timeout_client | default('50000ms') }}
timeout server {{ timeout_server | default('50000ms') }}
frontend http_front
bind *:{{ frontend_port | default(80) }}
default_backend app_servers
backend app_servers
balance {{ balance_method | default('roundrobin') }}
{% for host in groups['webservers'] %}
server {{ hostvars[host]['inventory_hostname_short'] }} {{ hostvars[host]['ansible_host'] }}:{{ backend_port | default(8080) }} check
{% endfor %}
Application Configuration Template
# templates/app.properties.j2
# {{ ansible_managed }}
# Generated on {{ ansible_date_time.iso8601 }}
# Environment: {{ environment }}
app.name={{ app_name }}
app.version={{ app_version }}
# Database configuration
{% if environment == 'production' %}
db.host={{ groups['db_primary'][0] }}
db.pool.size=50
db.pool.timeout=30000
{% else %}
db.host=localhost
db.pool.size=10
db.pool.timeout=5000
{% endif %}
db.name={{ db_name }}
db.user={{ db_user }}
db.password={{ db_password }}
# Redis cache servers
{% if groups['redis'] is defined %}
cache.enabled=true
cache.servers={% for host in groups['redis'] %}{{ hostvars[host]['ansible_host'] }}:6379{% if not loop.last %},{% endif %}{% endfor %}
{% else %}
cache.enabled=false
{% endif %}
# Feature flags
{% for flag, enabled in feature_flags.items() %}
feature.{{ flag }}={{ enabled | lower }}
{% endfor %}
Advanced Templating
Template Inheritance (Include)
# templates/base.conf.j2
# Base configuration
log_level = {{ log_level }}
# templates/app.conf.j2
{% include 'base.conf.j2' %}
# Additional app-specific config
app_port = {{ app_port }}
Macros (Reusable Blocks)
# templates/nginx_includes.j2
{% macro location_block(path, proxy_pass) %}
location {{ path }} {
proxy_pass {{ proxy_pass }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
{% endmacro %}
# templates/nginx_site.conf.j2
{% import 'nginx_includes.j2' as nginx %}
server {
listen 80;
server_name {{ server_name }};
{{ nginx.location_block('/api/', 'http://backend:8080') }}
{{ nginx.location_block('/admin/', 'http://admin:9000') }}
}
Dynamic Variable Names
# Access variables dynamically
{% set var_name = 'custom_' ~ environment %}
{{ vars[var_name] }}
# Loop through hostvars
{% for host in groups['all'] %}
{{ hostvars[host]['ansible_hostname'] }}
{% endfor %}
Template Security
Prevent Injection
# DANGEROUS - Don't do this
command = {{ user_input }}
# SAFE - Quote and escape
command = "{{ user_input | quote }}"
# SAFE - Use filters
sql_query = "SELECT * FROM users WHERE id = {{ user_id | int }}"
Ansible Managed Comments
# Add to top of templates
# {{ ansible_managed }}
# WARNING: This file is managed by Ansible. Manual changes will be overwritten.
# Customize in ansible.cfg
[defaults]
ansible_managed = Managed by Ansible on {host} at {time}
Template Validation
Validate Before Applying
---
- name: Deploy nginx config with validation
hosts: webservers
tasks:
- name: Template nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: 'nginx -t -c %s'
notify: reload nginx
- name: Deploy Apache config with validation
hosts: webservers
tasks:
- name: Template apache configuration
ansible.builtin.template:
src: apache.conf.j2
dest: /etc/apache2/sites-available/mysite.conf
validate: 'apachectl -t -f %s'
notify: reload apache
Backup Before Replace
---
- name: Safe configuration update
hosts: all
tasks:
- name: Template config with backup
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
backup: yes # Creates timestamped backup
register: config_update
- name: Show backup location
ansible.builtin.debug:
msg: "Backup saved to {{ config_update.backup_file }}"
when: config_update.backup_file is defined
Best Practices
Template Best Practices:
- Use .j2 Extension: Clearly identify template files
- Add ansible_managed: Mark files as Ansible-managed
- Validate Configs: Use validate parameter when possible
- Use Defaults: Provide sensible default values
- Comment Templates: Explain complex logic
- Test Thoroughly: Test with various variable combinations
- Version Control: Keep templates in version control
- Avoid Complex Logic: Keep templates simple; use variables
Debugging Templates
Test Template Rendering
---
- name: Debug template output
hosts: localhost
vars:
test_var: "test value"
tasks:
- name: Render template to stdout
ansible.builtin.template:
src: test.j2
dest: /tmp/test_output.txt
register: template_result
- name: Display rendered template
ansible.builtin.debug:
msg: "{{ lookup('template', 'test.j2') }}"
Use debug for Variable Inspection
---
- name: Inspect variables before templating
hosts: webservers
tasks:
- name: Show all variables
ansible.builtin.debug:
var: vars
- name: Show hostvars
ansible.builtin.debug:
var: hostvars[inventory_hostname]
- name: Show groups
ansible.builtin.debug:
var: groups
Next Steps
- Playbooks - Use templates in playbooks
- Variables - Master variable precedence
- Roles - Organize templates in roles
- Advanced Topics - Complex template patterns
- Try the Playground - Practice templating