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.j2

    ServerName {{ 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