Ansible Role Templates
Introduction
Templates are one of the most powerful features of Ansible roles. While static files have their place in configuration management, real-world scenarios often require dynamic content generation based on variables, host information, or other contextual data.
Ansible templates allow you to create configuration files dynamically by combining:
- Static text content
- Variable substitution
- Control structures (conditionals, loops)
- Filters for transforming data
Templates in Ansible roles use the Jinja2 templating engine, which provides a flexible way to generate configuration files tailored to each managed host. This dramatically reduces duplication and makes your infrastructure more maintainable.
Understanding the Templates Directory
Within an Ansible role structure, templates live in the templates
directory:
my_role/
├── defaults/
├── files/
├── handlers/
├── meta/
├── tasks/
├── templates/ <-- Template files go here
│ ├── nginx.conf.j2
│ └── app_config.ini.j2
├── tests/
└── vars/
By convention, template files use the .j2
extension (for Jinja2), though this isn't strictly required.
Basic Template Usage
Let's start with a simple example of using templates in an Ansible role. Imagine we're creating a role to deploy a web server.
Step 1: Create a template file
First, create a template file in your role's templates
directory:
templates/nginx.conf.j2
server {
listen {{ nginx_port | default(80) }};
server_name {{ nginx_server_name }};
root {{ nginx_document_root }};
index index.html;
{% if nginx_enable_ssl %}
listen 443 ssl;
ssl_certificate {{ nginx_ssl_cert_path }};
ssl_certificate_key {{ nginx_ssl_key_path }};
{% endif %}
}
Step 2: Define task to apply the template
In your role's tasks/main.yml
file:
- name: Create Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/sites-available/{{ nginx_server_name }}
owner: root
group: root
mode: '0644'
notify: Restart nginx
Step 3: Define default values
In your role's defaults/main.yml
:
nginx_port: 80
nginx_server_name: example.com
nginx_document_root: /var/www/html
nginx_enable_ssl: false
nginx_ssl_cert_path: /etc/ssl/certs/nginx.crt
nginx_ssl_key_path: /etc/ssl/private/nginx.key
This simple example demonstrates how templates can generate different configuration files based on variables.
Template Features and Syntax
Variable Substitution
The most basic feature of templates is variable substitution using {{ variable_name }}
syntax:
app_name: {{ application_name }}
environment: {{ env | default('production') }}
debug_mode: {{ debug | default(false) }}
Conditionals
You can use conditionals to include or exclude sections of your configuration:
{% if backup_enabled %}
backup_path: {{ backup_directory }}
backup_schedule: {{ backup_schedule }}
{% endif %}
{% if env == 'development' %}
loglevel: DEBUG
{% elif env == 'staging' %}
loglevel: INFO
{% else %}
loglevel: WARNING
{% endif %}
Loops
Loops are useful for generating repeated configuration sections:
allowed_hosts:
{% for host in allowed_host_list %}
- {{ host }}
{% endfor %}
{% for user in system_users %}
user_{{ user.name }}:
home: {{ user.home | default('/home/' + user.name) }}
shell: {{ user.shell | default('/bin/bash') }}
groups: {{ user.groups | join(',') }}
{% endfor %}
Filters
Jinja2 provides many filters to transform data:
max_connections: {{ max_connections | default(100) }}
app_version: {{ version | trim }}
server_list: {{ servers | join(',') }}
config_json: {{ config_dict | to_json }}
uppercase_name: {{ app_name | upper }}
Real-World Example: Database Configuration Role
Let's create a more comprehensive example for configuring a database across different environments.
Directory Structure
database_role/
├── defaults/
│ └── main.yml
├── templates/
│ ├── my.cnf.j2
│ └── backup.sh.j2
└── tasks/
└── main.yml
Default Variables
defaults/main.yml
# Database configuration
db_type: mysql
db_port: 3306
db_bind_address: 127.0.0.1
db_max_connections: 100
db_innodb_buffer_pool_size: 128M
# Backup configuration
db_backup_enabled: true
db_backup_directory: /var/backups/mysql
db_backup_retention_days: 7
db_backup_time: "23:00"
# Environment-specific settings
db_environments:
development:
max_connections: 50
innodb_buffer_pool_size: 64M
staging:
max_connections: 100
innodb_buffer_pool_size: 128M
production:
max_connections: 200
innodb_buffer_pool_size: 512M
Template Files
templates/my.cnf.j2
[mysqld]
port = {{ db_port }}
bind-address = {{ db_bind_address }}
# Performance settings
max_connections = {% if db_environment in db_environments %}{{ db_environments[db_environment].max_connections }}{% else %}{{ db_max_connections }}{% endif %}
innodb_buffer_pool_size = {% if db_environment in db_environments %}{{ db_environments[db_environment].innodb_buffer_pool_size }}{% else %}{{ db_innodb_buffer_pool_size }}{% endif %}
# Security settings
{% if db_environment == 'production' %}
ssl = ON
ssl-ca = {{ db_ssl_ca }}
ssl-cert = {{ db_ssl_cert }}
ssl-key = {{ db_ssl_key }}
{% endif %}
# Custom options
{% if db_custom_options is defined %}
{% for option in db_custom_options %}
{{ option.key }} = {{ option.value }}
{% endfor %}
{% endif %}
templates/backup.sh.j2
#!/bin/bash
# Backup script for {{ db_type }} database
# Generated by Ansible on {{ ansible_date_time.date }}
BACKUP_DIR="{{ db_backup_directory }}"
RETENTION_DAYS={{ db_backup_retention_days }}
DATE=$(date +%Y%m%d)
HOSTNAME=$(hostname)
{% if db_type == 'mysql' %}
mysqldump --all-databases -u root -p"${MYSQL_ROOT_PASSWORD}" > ${BACKUP_DIR}/${HOSTNAME}-${DATE}.sql
{% elif db_type == 'postgresql' %}
pg_dumpall -U postgres > ${BACKUP_DIR}/${HOSTNAME}-${DATE}.sql
{% endif %}
# Compress the backup
gzip ${BACKUP_DIR}/${HOSTNAME}-${DATE}.sql
# Clean up old backups
find ${BACKUP_DIR} -name "*.gz" -mtime +${RETENTION_DAYS} -delete
{% if db_notify_email is defined %}
# Send notification
echo "Database backup completed on ${HOSTNAME}" | mail -s "Backup Notification" {{ db_notify_email }}
{% endif %}
Tasks
tasks/main.yml
---
- name: Ensure backup directory exists
file:
path: "{{ db_backup_directory }}"
state: directory
mode: '0750'
when: db_backup_enabled
- name: Configure database
template:
src: my.cnf.j2
dest: /etc/{{ db_type }}/my.cnf
owner: root
group: root
mode: '0644'
notify: Restart database service
- name: Set up database backup script
template:
src: backup.sh.j2
dest: /usr/local/bin/db-backup.sh
owner: root
group: root
mode: '0755'
when: db_backup_enabled
- name: Set up backup cron job
cron:
name: "Database backup"
hour: "{{ db_backup_time.split(':')[0] }}"
minute: "{{ db_backup_time.split(':')[1] }}"
job: "/usr/local/bin/db-backup.sh"
state: "{{ 'present' if db_backup_enabled else 'absent' }}"
Template Best Practices
1. Keep Templates Simple
While Jinja2 provides powerful programming capabilities, complex logic in templates can make them hard to maintain. When possible:
- Move complex transformations to your playbook or role variables
- Use default filters to handle missing variables
- Break complex templates into smaller, manageable pieces
2. Comment Your Templates
Add comments to explain non-obvious sections:
# This section configures the connection pool
# Increase max_connections for high-traffic environments
max_connections = {{ max_connections }}
3. Use Defensive Programming
Account for missing or undefined variables:
debug = {{ debug_mode | default(false) }}
log_dir = {{ log_directory | default('/var/log/' + application_name) }}
4. Validate Generated Files
For complex templates, add validation tasks:
- name: Validate Nginx configuration
command: nginx -t -c /etc/nginx/nginx.conf
changed_when: false
register: nginx_valid
failed_when: nginx_valid.rc != 0
5. Consider Template Inheritance
For complex configurations, leverage Jinja2's template inheritance:
templates/base.conf.j2
# Base configuration
log_level = {{ log_level }}
data_dir = {{ data_dir }}
{% block custom_config %}
# Custom configuration goes here
{% endblock %}
templates/app.conf.j2
{% extends "base.conf.j2" %}
{% block custom_config %}
app_name = {{ app_name }}
app_port = {{ app_port }}
{% endblock %}
Advanced Template Techniques
Using Registered Variables
You can use facts or registered variables from previous tasks:
- name: Get disk information
command: df -h
register: disk_info
changed_when: false
- name: Create report
template:
src: system_report.j2
dest: /opt/reports/system_status.txt
Then in your template:
System Report
-------------
Generated: {{ ansible_date_time.iso8601 }}
Hostname: {{ ansible_hostname }}
Disk Usage:
{{ disk_info.stdout }}
Template Debugging
When templates don't render as expected, you can debug the variables:
- name: Debug variables
debug:
var: some_complex_variable
Or inside the template itself:
{# Debug information #}
{# {{ some_variable | to_yaml }} #}
Using Lookups in Templates
You can use Ansible lookups within templates:
# Database configuration from vault
db_password: {{ lookup('ansible.builtin.passwordstore', 'path/to/password') }}
Creating Reusable Role Templates
Template Organization
For complex roles, organize templates in subdirectories:
templates/
├── apache/
│ ├── vhost.conf.j2
│ └── security.conf.j2
├── mysql/
│ ├── my.cnf.j2
│ └── users.sql.j2
└── common/
└── backup.sh.j2
Template Sharing Between Roles
For templates used across multiple roles, consider:
- Creating a common role with shared templates
- Using the
template_src
variable to specify alternative templates
- name: Configure web server
template:
src: "{{ web_config_template | default('nginx.conf.j2') }}"
dest: /etc/nginx/sites-available/default
This allows role users to override the template while keeping your default.
Workflow Diagram
Here's a visual representation of how templates work in Ansible roles:
Summary
Templates are the backbone of creating dynamic, adaptable configurations in Ansible roles. By leveraging Jinja2's powerful features, you can generate configuration files customized for each environment and host in your infrastructure.
Key points to remember:
- Templates use the
.j2
extension and live in thetemplates/
directory - The Jinja2 templating language provides variable substitution, conditionals, loops, and filters
- Well-designed templates make your roles more reusable and adaptable
- Always validate generated configurations before applying them
- Use defensive programming techniques to handle missing variables
Exercises
-
Basic Template: Create a template file for a simple application configuration that sets different log levels based on the environment.
-
Loop Exercise: Write a template that generates a list of virtual hosts for Apache based on a list of websites defined in variables.
-
Conditional Exercise: Create a template that includes different security settings depending on whether the server is internet-facing or internal.
-
Advanced Exercise: Design a template for a complex application that uses inheritance to share common settings while allowing environment-specific customizations.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)