Module 05: Templates

๐ŸŽฏ Learning Objectives

By the end of this module, you will:

  • Master Jinja2 templating syntax for dynamic configuration files
  • Use variables, conditionals, and loops within templates
  • Apply filters for data transformation and formatting
  • Handle whitespace control and template organization
  • Debug template issues and troubleshoot rendering problems
  • Create reusable template patterns for common scenarios
  • Integrate templates with Ansible's template module effectively

๐Ÿ“‹ Why Templates Transform Configuration Management

Static vs Dynamic Configuration

Static Configuration Files: Hard to maintain across environments

# httpd.conf - Hard-coded values
ServerName web01.example.com
Listen 80
MaxRequestWorkers 150
DocumentRoot /var/www/html

Dynamic Template-Based Configuration: Adaptable and maintainable

# httpd.conf.j2 - Template with variables
ServerName {{ ansible_fqdn }}
Listen {{ http_port | default(80) }}
MaxRequestWorkers {{ max_workers | default(150) }}
DocumentRoot {{ document_root | default('/var/www/html') }}

Template Benefits

  • Environment Flexibility: Same template works across dev/staging/production
  • Maintainability: Change logic in one place, affects all deployments
  • Dynamic Content: Generate configuration based on runtime conditions
  • Consistency: Standardized configuration patterns across infrastructure

๐ŸŽจ Jinja2 Template Fundamentals

Template File Structure

File Naming Convention: .j2 extension (e.g., httpd.conf.j2)

Basic Template Structure:

{# templates/httpd.conf.j2 #}
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
# Managed by template: httpd.conf.j2

ServerName {{ ansible_fqdn }}
ServerAdmin {{ server_admin | default('webmaster@' + ansible_domain) }}

Listen {{ http_port }}
{% if ssl_enabled | default(false) %}
Listen {{ https_port | default(443) }} ssl
{% endif %}

# Virtual Hosts
{% for vhost in virtual_hosts | default([]) %}
<VirtualHost *:{{ http_port }}>
    ServerName {{ vhost.name }}
    DocumentRoot {{ vhost.docroot }}
    {% if vhost.aliases is defined %}
    ServerAlias {{ vhost.aliases | join(' ') }}
    {% endif %}
</VirtualHost>
{% endfor %}

Jinja2 Syntax Elements

Variable Substitution:

{{ variable_name }}                    # Simple variable
{{ ansible_facts['hostname'] }}       # Dictionary access
{{ users[0]['name'] }}                # List and dictionary access
{{ config.database.host }}            # Dot notation for dictionaries

Comments:

{# This is a single-line comment #}

{#
  This is a 
  multi-line comment
#}

{# TODO: Add SSL configuration #}

Control Structures:

# Conditionals
{% if condition %}
content
{% endif %}

# Loops
{% for item in list %}
content with {{ item }}
{% endfor %}

# Assignments
{% set variable = value %}

๐Ÿ”ง Variable Usage in Templates

Basic Variable Substitution

{# Basic variable usage #}
ServerName {{ server_name }}
Port {{ server_port }}
User {{ web_user }}
Group {{ web_group }}

{# With default values #}
MaxClients {{ max_clients | default(256) }}
Timeout {{ timeout | default(300) }}
KeepAlive {{ keepalive | default('On') }}

{# Conditional defaults #}
LogLevel {{ log_level | default('warn' if environment == 'production' else 'info') }}

Complex Data Structures

Dictionary Access:

{# Dictionary variable: database = {host: 'db.example.com', port: 3306, name: 'webapp'} #}

# Database Configuration
Host={{ database.host }}
Port={{ database.port }}
Database={{ database.name }}
Username={{ database.user | default('app') }}
Password={{ database.password }}

# Alternative bracket notation
Host={{ database['host'] }}
Port={{ database['port'] }}
Database={{ database['name'] }}

List Processing:

{# List variable: allowed_hosts = ['web01.example.com', 'web02.example.com', 'api.example.com'] #}

# Simple list output
AllowedHosts={{ allowed_hosts | join(',') }}

# List with formatting
{% for host in allowed_hosts %}
Allow from {{ host }}
{% endfor %}

# List with conditionals
{% for host in allowed_hosts %}
{% if 'api' not in host %}
<VirtualHost {{ host }}:80>
    DocumentRoot /var/www/html
</VirtualHost>
{% endif %}
{% endfor %}

Ansible Facts in Templates

System Information:

# System Facts Template
# Generated for {{ ansible_facts['hostname'] }}
# OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
# Architecture: {{ ansible_facts['architecture'] }}
# Total Memory: {{ ansible_facts['memtotal_mb'] }}MB
# CPU Count: {{ ansible_facts['processor_count'] }}

# Network configuration based on facts
{% if ansible_facts['default_ipv4'] is defined %}
BindAddress={{ ansible_facts['default_ipv4']['address'] }}
{% endif %}

# Storage-based configuration
{% for mount in ansible_facts['mounts'] %}
{% if mount['mount'] == '/' %}
# Root filesystem: {{ mount['fstype'] }}, Size: {{ (mount['size_total'] / 1024 / 1024 / 1024) | round(1) }}GB
{% endif %}
{% endfor %}

Hardware-Based Configuration:

# Performance tuning based on hardware
{% set memory_mb = ansible_facts['memtotal_mb'] %}
{% set cpu_count = ansible_facts['processor_count'] %}

# Memory-based worker configuration
{% if memory_mb < 2048 %}
workers=2
max_connections=50
{% elif memory_mb < 8192 %}
workers={{ cpu_count * 2 }}
max_connections=200
{% else %}
workers={{ cpu_count * 4 }}
max_connections=500
{% endif %}

# CPU-based thread configuration
thread_pool_size={{ cpu_count * 8 }}

๐ŸŽ›๏ธ Control Structures in Templates

Conditional Logic

If/Elif/Else Statements:

{# Environment-based configuration #}
{% if environment == 'development' %}
debug=true
log_level=debug
cache_enabled=false
{% elif environment == 'staging' %}
debug=false
log_level=info
cache_enabled=true
cache_ttl=300
{% elif environment == 'production' %}
debug=false
log_level=warn
cache_enabled=true
cache_ttl=3600
{% else %}
debug=false
log_level=error
cache_enabled=false
{% endif %}

{# Feature flags #}
{% if ssl_enabled | default(false) %}
# SSL Configuration
SSLEngine on
SSLCertificateFile {{ ssl_cert_path }}
SSLCertificateKeyFile {{ ssl_key_path }}
{% if ssl_intermediate_path is defined %}
SSLCertificateChainFile {{ ssl_intermediate_path }}
{% endif %}
{% endif %}

Inline Conditionals:

# Inline conditional expressions
ServerTokens={{ 'Prod' if environment == 'production' else 'Full' }}
MaxRequestWorkers={{ 500 if high_traffic | default(false) else 150 }}

# Conditional sections
{% if backup_enabled | default(true) %}backup_path={{ backup_directory }}{% endif %}

Loop Constructs

Basic For Loops:

{# Simple list iteration #}
# Virtual Hosts
{% for vhost in virtual_hosts %}
<VirtualHost *:80>
    ServerName {{ vhost }}
    DocumentRoot /var/www/{{ vhost }}/html
</VirtualHost>
{% endfor %}

{# Dictionary iteration #}
# Environment Variables
{% for key, value in env_vars.items() %}
export {{ key }}="{{ value }}"
{% endfor %}

# Alternative dictionary syntax
{% for item in env_vars | dictsort %}
export {{ item[0] }}="{{ item[1] }}"
{% endfor %}

Advanced Loop Features:

{# Loop variables and controls #}
# User accounts ({{ users | length }} total)
{% for user in users %}
{% set loop_info = loop %}  {# Capture loop context #}
# User {{ loop.index }}/{{ loop.length }}: {{ user.name }}
username={{ user.name }}
uid={{ user.uid | default(1000 + loop.index0) }}
groups={{ user.groups | default(['users']) | join(',') }}
{% if user.shell is defined %}
shell={{ user.shell }}
{% endif %}

{% if not loop.last %}

{% endif %}  {# Add blank line between users except last #}
{% endfor %}

{# Conditional loop content #}
# Active services
{% for service in services %}
{% if service.enabled | default(true) %}
[{{ service.name }}]
port={{ service.port }}
{% if service.ssl | default(false) %}
ssl_enabled=yes
ssl_port={{ service.ssl_port | default(service.port + 443) }}
{% endif %}
{% endif %}
{% endfor %}

Nested Loops:

{# Complex nested structure #}
# Load Balancer Configuration
{% for cluster in clusters %}
# Cluster: {{ cluster.name }}
{% for backend in cluster.backends %}
server {{ backend.name }} {{ backend.ip }}:{{ backend.port }} {% if backend.backup | default(false) %}backup{% endif %}
{% endfor %}
{% if not loop.last %}

{% endif %}
{% endfor %}

{# Loop with conditions #}
# Firewall Rules
{% for zone in firewall_zones %}
{% for rule in zone.rules %}
{% if rule.enabled | default(true) %}
-A {{ zone.name }} -p {{ rule.protocol }} --dport {{ rule.port }} -j ACCEPT
{% endif %}
{% endfor %}
{% endfor %}

๐Ÿ” Filters and Data Transformation

Built-in Filters

String Manipulation:

# String filters
hostname={{ inventory_hostname | upper }}
username={{ user_name | lower }}
service_name={{ app_name | title }}
config_path={{ base_path | trim }}

# String formatting
server_id={{ inventory_hostname | replace('.', '_') }}
log_file={{ app_name | lower | replace(' ', '_') }}.log
backup_name={{ ansible_date_time.date | replace('-', '') }}_backup.sql

# String tests and defaults
database_url={{ db_url | default('localhost:5432') }}
api_key={{ api_key | default('REPLACE_ME', true) }}  # true = treat empty string as undefined

Numeric Filters:

# Math operations
max_memory={{ (ansible_facts['memtotal_mb'] * 0.8) | round | int }}MB
worker_count={{ (ansible_facts['processor_count'] * 2) | round }}
cache_size={{ memory_limit | int // 4 }}

# Formatting
disk_size={{ (disk_bytes / 1024 / 1024 / 1024) | round(2) }}GB
percentage={{ (used_space / total_space * 100) | round(1) }}%

List and Dictionary Filters:

# List operations
all_hosts={{ groups['all'] | sort | join(',') }}
web_servers={{ groups['webservers'] | length }} servers
first_db={{ groups['databases'] | first }}
backup_hosts={{ groups['all'] | difference(groups['excluded'] | default([])) }}

# List filtering
active_services={{ services | selectattr('enabled') | map(attribute='name') | list }}
https_ports={{ services | selectattr('ssl', 'equalto', true) | map(attribute='port') | list }}

# Dictionary operations
sorted_config={{ config_dict | dictsort }}
config_keys={{ config_dict | list }}  # Get keys only
config_values={{ config_dict.values() | list }}

Date and Time Filters:

# Date formatting
generated_on={{ ansible_date_time.iso8601 }}
backup_timestamp={{ ansible_date_time.epoch }}
human_date={{ ansible_date_time.date }} at {{ ansible_date_time.time }}

# Custom date formatting (requires strftime)
log_rotation_date={{ ansible_date_time.iso8601 | regex_replace('(\d{4})-(\d{2})-(\d{2}).*', '\\1\\2\\3') }}

Custom Filter Applications

Configuration Logic Filters:

# Complex conditional logic
{% set ssl_config = ssl_enabled | default(false) %}
{% set port_config = (443 if ssl_config else 80) %}

# Environment-based values
debug_mode={{ (environment != 'production') | lower }}
log_level={{ {'development': 'debug', 'staging': 'info', 'production': 'warn'}[environment] }}

# Resource calculations
{% set memory_ratio = 0.7 if environment == 'production' else 0.5 %}
max_heap_size={{ (ansible_facts['memtotal_mb'] * memory_ratio) | int }}m

Network and IP Filters:

# IP address manipulation
network_interface={{ ansible_facts['default_ipv4']['interface'] }}
server_ip={{ ansible_facts['default_ipv4']['address'] }}
subnet_mask={{ ansible_facts['default_ipv4']['netmask'] }}

# Network calculations (custom logic)
{% set ip_parts = ansible_facts['default_ipv4']['address'].split('.') %}
network_id={{ ip_parts[0] }}.{{ ip_parts[1] }}.{{ ip_parts[2] }}.0

โš™๏ธ Template Module Usage

Basic Template Task

- name: Deploy Apache configuration
  ansible.builtin.template:
    src: httpd.conf.j2
    dest: /etc/httpd/conf/httpd.conf
    owner: root
    group: root
    mode: '0644'
    backup: yes
  notify: restart apache

Advanced Template Options

- name: Deploy complex configuration with validation
  ansible.builtin.template:
    src: "{{ item.template }}"
    dest: "{{ item.dest }}"
    owner: "{{ item.owner | default('root') }}"
    group: "{{ item.group | default('root') }}"
    mode: "{{ item.mode | default('0644') }}"
    backup: "{{ create_backups | default(true) }}"
    validate: "{{ item.validate | default(omit) }}"
  loop:
    - template: httpd.conf.j2
      dest: /etc/httpd/conf/httpd.conf
      validate: 'httpd -t -f %s'
    - template: ssl.conf.j2
      dest: /etc/httpd/conf.d/ssl.conf
      validate: 'httpd -t -f /etc/httpd/conf/httpd.conf'
    - template: vhosts.conf.j2
      dest: /etc/httpd/conf.d/vhosts.conf
  notify: restart apache

Template with Variables

- name: Deploy environment-specific configuration
  ansible.builtin.template:
    src: app.conf.j2
    dest: "/etc/myapp/{{ environment }}.conf"
    owner: myapp
    group: myapp
    mode: '0600'
  vars:
    app_config:
      database:
        host: "{{ db_host }}"
        port: "{{ db_port | default(5432) }}"
        name: "{{ app_name }}_{{ environment }}"
      cache:
        enabled: "{{ environment == 'production' }}"
        ttl: "{{ cache_ttl | default(3600) }}"
      logging:
        level: "{{ log_levels[environment] }}"
        file: "/var/log/myapp/{{ environment }}.log"
  notify: restart myapp

๐ŸŽจ Advanced Template Patterns

Template Inheritance and Includes

Base Template (base.conf.j2):

# Base configuration template
# Application: {{ app_name }}
# Environment: {{ environment }}
# Generated: {{ ansible_date_time.iso8601 }}

[global]
app_name={{ app_name }}
environment={{ environment }}
debug={{ debug_mode | default(false) | lower }}

{% block database_config %}
[database]
host={{ database.host }}
port={{ database.port }}
name={{ database.name }}
{% endblock %}

{% block cache_config %}
# Cache configuration will be included here
{% endblock %}

{% block custom_config %}
# Custom configuration can be added here
{% endblock %}

Extended Template (production.conf.j2):

{% extends "base.conf.j2" %}

{% block cache_config %}
[cache]
enabled=true
type=redis
host={{ redis_host }}
port={{ redis_port | default(6379) }}
ttl={{ cache_ttl | default(3600) }}
{% endblock %}

{% block custom_config %}
[monitoring]
enabled=true
endpoint={{ monitoring_endpoint }}
interval={{ monitoring_interval | default(60) }}

[security]
ssl_required=true
encryption_key={{ vault_encryption_key }}
{% endblock %}

Macro Definition and Usage

{# Macro definitions #}
{% macro render_vhost(name, docroot, port=80, ssl=false) %}
<VirtualHost *:{{ port }}>
    ServerName {{ name }}
    DocumentRoot {{ docroot }}
    {% if ssl %}
    SSLEngine on
    SSLCertificateFile /etc/ssl/certs/{{ name }}.crt
    SSLCertificateKeyFile /etc/ssl/private/{{ name }}.key
    {% endif %}

    ErrorLog logs/{{ name }}_error.log
    CustomLog logs/{{ name }}_access.log common
</VirtualHost>
{% endmacro %}

{# Macro usage #}
{% for vhost in virtual_hosts %}
{{ render_vhost(vhost.name, vhost.docroot, vhost.port | default(80), vhost.ssl | default(false)) }}
{% endfor %}

Dynamic Content Generation

{# Dynamic firewall rules based on services #}
{% set firewall_rules = [] %}
{% for service in services %}
  {% if service.external_access | default(false) %}
    {% set _ = firewall_rules.append('-A INPUT -p tcp --dport ' + service.port|string + ' -j ACCEPT') %}
  {% else %}
    {% set _ = firewall_rules.append('-A INPUT -s 192.168.0.0/16 -p tcp --dport ' + service.port|string + ' -j ACCEPT') %}
  {% endif %}
{% endfor %}

# Generated firewall rules
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]

# Allow loopback
-A INPUT -i lo -j ACCEPT

# Allow established connections
-A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Service-specific rules
{% for rule in firewall_rules %}
{{ rule }}
{% endfor %}

COMMIT

๐Ÿ› Template Debugging and Troubleshooting

Common Template Issues

Variable Undefined Errors:

{# Problem: Variable might be undefined #}
ServerName {{ server_name }}  # Fails if server_name is undefined

{# Solution: Use default values #}
ServerName {{ server_name | default(ansible_fqdn) }}

{# Solution: Check if defined #}
{% if server_name is defined %}
ServerName {{ server_name }}
{% endif %}

Type Errors:

{# Problem: Wrong data type #}
MaxClients {{ max_clients }}  # Fails if max_clients is a string

{# Solution: Type conversion #}
MaxClients {{ max_clients | int }}

{# Solution: Type checking #}
{% if max_clients is number %}
MaxClients {{ max_clients }}
{% else %}
MaxClients 150
{% endif %}

Loop Issues:

{# Problem: Empty list causes no output #}
{% for user in users %}
User: {{ user }}
{% endfor %}

{# Solution: Check for empty lists #}
{% if users | length > 0 %}
# User configuration
{% for user in users %}
User: {{ user }}
{% endfor %}
{% else %}
# No users configured
{% endif %}

Debugging Techniques

Debug Output in Templates:

{# Debug information #}
<!-- Template Debug Information
  Template: {{ template_path | default('unknown') }}
  Host: {{ inventory_hostname }}
  Variables:
  {% for key, value in vars.items() %}
  {{ key }}: {{ value }}
  {% endfor %}
-->

{# Conditional debug sections #}
{% if debug_templates | default(false) %}
# DEBUG: Variable dump
{% for key in vars.keys() | sort %}
# {{ key }} = {{ vars[key] }}
{% endfor %}
{% endif %}

Template Testing Playbook:

---
- name: Test template rendering
  hosts: localhost
  vars:
    debug_templates: true
    test_vars:
      server_name: test.example.com
      port: 8080
      ssl_enabled: false
  tasks:
    - name: Generate test template
      ansible.builtin.template:
        src: httpd.conf.j2
        dest: /tmp/httpd_test.conf
      vars: "{{ test_vars }}"

    - name: Display rendered template
      ansible.builtin.debug:
        msg: "{{ lookup('file', '/tmp/httpd_test.conf') }}"

Whitespace Control

{# Whitespace control examples #}

{# Remove whitespace before #}
{% for item in list -%}
{{ item }}
{% endfor %}

{# Remove whitespace after #}
{% for item in list %}
{{ item }}
{%- endfor %}

{# Remove whitespace both sides #}
{%- for item in list -%}
{{ item }}
{%- endfor -%}

{# Practical example: clean list output #}
hosts={{ groups['webservers'] | join(',') }}

{# vs. #}
hosts=
{%- for host in groups['webservers'] -%}
{{ host }}
{%- if not loop.last -%},{%- endif -%}
{%- endfor %}

๐Ÿงช Practical Lab Exercises

Exercise 1: Multi-Environment Web Server Configuration

Create templates for Apache that:

  1. Adapt to different environments (dev/staging/production)
  2. Configure SSL based on variables
  3. Generate virtual hosts dynamically
  4. Include environment-specific tuning parameters
{# httpd.conf.j2 #}
# Apache Configuration for {{ environment | upper }}
# Generated: {{ ansible_date_time.iso8601 }}

ServerRoot /etc/httpd
PidFile run/httpd.pid

# Environment-specific settings
{% if environment == 'production' %}
ServerTokens Prod
ServerSignature Off
MaxRequestWorkers 500
{% elif environment == 'staging' %}
ServerTokens Min
ServerSignature Off
MaxRequestWorkers 250
{% else %}
ServerTokens Full
ServerSignature On
MaxRequestWorkers 150
{% endif %}

# SSL Configuration
{% if ssl_enabled | default(false) %}
LoadModule ssl_module modules/mod_ssl.so
Include conf.d/ssl.conf
{% endif %}

# Virtual Hosts
{% for vhost in virtual_hosts %}
{% include 'vhost.conf.j2' %}
{% endfor %}

Exercise 2: Database Configuration Template

Build a template that:

  1. Configures connection pools based on available memory
  2. Sets logging levels by environment
  3. Includes backup configuration conditionally
  4. Generates connection strings for multiple databases

Exercise 3: Load Balancer Configuration

Create a template that:

  1. Discovers backend servers from inventory
  2. Configures health checks based on service type
  3. Sets up SSL termination conditionally
  4. Generates monitoring configuration

Exercise 4: Complex Service Configuration

Design templates for:

  1. Application server with multiple instances
  2. Service discovery integration
  3. Feature flag configuration
  4. Performance tuning based on hardware facts

๐ŸŽฏ Key Takeaways

Template Design Excellence

  • Variable usage: Always provide defaults and check for undefined variables
  • Logic organization: Keep complex logic in templates minimal and readable
  • Reusability: Design templates that work across environments
  • Documentation: Include comments explaining template logic

Jinja2 Mastery

  • Syntax proficiency: Master variables, conditionals, loops, and filters
  • Data handling: Understand how to process lists, dictionaries, and complex structures
  • Whitespace control: Manage template output formatting effectively
  • Debugging skills: Know how to troubleshoot template rendering issues

Configuration Management

  • Environment adaptation: Templates should adapt to different deployment environments
  • Fact integration: Leverage Ansible facts for intelligent configuration
  • Validation: Use template validation to catch configuration errors
  • Security: Handle sensitive data appropriately in templates

Best Practices

  • Testing: Always test templates in non-production environments first
  • Backup strategy: Use backup options to prevent configuration loss
  • Version control: Track template changes alongside playbook changes
  • Performance: Consider template rendering performance for large deployments

๐Ÿ”— Next Steps

With template mastery achieved, you're ready for:

  1. Module 06: Roles - Organize templates and tasks into reusable roles
  2. Role-based templates that can be shared across projects
  3. Template libraries for common configuration patterns
  4. Advanced role patterns with template inheritance and customization

Your template skills enable sophisticated configuration management!


Next Module: Module 06: Roles โ†’