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:
- Adapt to different environments (dev/staging/production)
- Configure SSL based on variables
- Generate virtual hosts dynamically
- 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:
- Configures connection pools based on available memory
- Sets logging levels by environment
- Includes backup configuration conditionally
- Generates connection strings for multiple databases
Exercise 3: Load Balancer Configuration¶
Create a template that:
- Discovers backend servers from inventory
- Configures health checks based on service type
- Sets up SSL termination conditionally
- Generates monitoring configuration
Exercise 4: Complex Service Configuration¶
Design templates for:
- Application server with multiple instances
- Service discovery integration
- Feature flag configuration
- 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:
- Module 06: Roles - Organize templates and tasks into reusable roles
- Role-based templates that can be shared across projects
- Template libraries for common configuration patterns
- Advanced role patterns with template inheritance and customization
Your template skills enable sophisticated configuration management!
Next Module: Module 06: Roles โ