Understanding Ansible Templates: A Comprehensive Guide
Templates solve a fundamental challenge in configuration management: how to maintain consistency while accommodating necessary variations across systems.
Ansible templates are configuration files with dynamic elements that get rendered at runtime.
Unlike static files that remain identical when copied to target systems, templates can incorporate variables, conditionals, loops, and other programming constructs to generate customized files for each managed host.
For example, a web server configuration might need the same overall structure across all servers, but with different IP addresses, port settings, or resource allocations depending on the environment (development, testing, or production) and the specific role of each server.
In this comprehensive guide, we'll explore how Ansible templates work, see practical examples, and learn best practices for effective implementation.
Understanding Jinja2 template engine basics
Ansible templates use Jinja2, a modern and designer-friendly templating language for Python.
It provides the syntax and processing capabilities that make templates dynamic. Let's examine the fundamental elements of Jinja2 that you'll use in your Ansible templates.
Variables and expressions
Variables in Jinja2 are enclosed in double curly braces: {{ variable_name }}
.
When Ansible processes a template, it replaces these variables with their actual
values from your inventory, playbooks, or role definitions.
For example, a simple template for an Apache virtual host might look like this:
<VirtualHost *:{{ http_port }}>
ServerAdmin {{ admin_email }}
ServerName {{ server_name }}
DocumentRoot {{ doc_root }}
ErrorLog ${APACHE_LOG_DIR}/{{ server_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/{{ server_name }}_access.log combined
</VirtualHost>
When this template is processed, Ansible replaces the variables with their
actual values. If http_port
is set to 80, admin_email
to
admin@example.com
, server_name
to www.example.com
, and doc_root
to
"/var/www/html", the resulting file would be:
<VirtualHost *:80>
ServerAdmin admin@example.com
ServerName www.example.com
DocumentRoot /var/www/html
ErrorLog ${APACHE_LOG_DIR}/www.example.com_error.log
CustomLog ${APACHE_LOG_DIR}/www.example.com_access.log combined
</VirtualHost>
Control structures
Jinja2 offers control structures that enable you to add logic to your templates.
These structures include conditionals and loops, which are enclosed in {% %}
delimiters.
Conditionals
Conditionals let you include or exclude content based on certain conditions:
server {
listen {{ nginx_port }};
server_name {{ server_name }};
{% if ssl_enabled %}
ssl on;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
location / {
root {{ web_root }};
index index.html;
}
}
In this example, the SSL configuration is only included if the ssl_enabled
variable is set to true.
Loops
Loops allow you to iterate over collections like lists and dictionaries, generating repetitive content dynamically:
frontend http_front
bind *:80
default_backend http_back
backend http_back
balance roundrobin
{% for server in web_servers %}
server {{ server.name }} {{ server.ip }}:{{ server.port }} weight={{ server.weight }}
{% endfor %}
This template would generate a list of backend servers in HAProxy configuration
based on the web_servers
list variable.
Filters
Filters transform data before it's rendered in the template. They are applied
using the pipe symbol (|
):
Welcome to {{ ansible_hostname | upper }}
System is running {{ ansible_distribution }} {{ ansible_distribution_version }}
Today's date is {{ ansible_date_time.date }}
In this example, the upper
filter converts the hostname to uppercase. Ansible
has many built-in filters for string manipulation, data transformation, and more
complex operations.
Creating your first Ansible template
Now that you understand the basics, let's create a complete example of using templates in an Ansible playbook. We'll create a template for an Nginx virtual host configuration.
Step 1: Create the template file
First, create a templates directory in your Ansible project and add a new file
named nginx_vhost.conf.j2
:
server {
listen {{ nginx_port }} default_server;
server_name {{ domain_name }};
root {{ document_root }};
index index.html index.htm;
access_log /var/log/nginx/{{ domain_name }}_access.log;
error_log /var/log/nginx/{{ domain_name }}_error.log;
location / {
try_files $uri $uri/ =404;
}
{% if enable_php %}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
}
{% endif %}
}
Step 2: Define variables
Next, define the variables either in your inventory, groupvars, hostvars, or directly in the playbook. Here's an example using variables in a playbook:
- name: Configure nginx virtual host
hosts: web_servers
become: yes
vars:
nginx_port: 80
domain_name: "example.com"
document_root: "/var/www/html"
enable_php: true
tasks:
- name: Create Nginx virtual host configuration
template:
src: nginx_vhost.conf.j2
dest: /etc/nginx/sites-available/{{ domain_name }}.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
- name: Enable site
file:
src: /etc/nginx/sites-available/{{ domain_name }}.conf
dest: /etc/nginx/sites-enabled/{{ domain_name }}.conf
state: link
notify: Restart nginx
handlers:
- name: Restart nginx
service:
name: nginx
state: restarted
Step 3: Run the playbook
Execute the playbook to deploy the template:
ansible-playbook nginx_template_playbook.yml
The template module will process the template file, replace all variables and expressions with their values, and write the resulting configuration to the destination path on the target servers.
Variables in templates
Understanding variable sources and precedence is crucial when working with Ansible templates. Variables can come from multiple places, and knowing which value takes precedence can save you hours of debugging.
Variable sources
Variables in Ansible can come from:
- Inventory files (both static and dynamic).
- Group variables in
group_vars/
directory. - Host variables in
host_vars/
directory. - Variables defined in playbooks.
- Variables defined in roles.
- Extra variables passed at the command line with
-e
or--extra-vars
. - Facts gathered from remote systems.
Let's see a practical example of organizing variables for a multi-environment setup:
[web_servers]
web1.example.com
web2.example.com
[db_servers]
db1.example.com
---
http_port: 80
enable_ssl: true
document_root: /var/www/production
max_clients: 500
---
server_priority: high
backup_enabled: true
When these variables are used in a template, Ansible combines them according to
its variable precedence rules. Variables defined in hostvars override those in
groupvars, and variables passed at runtime with -e
override almost everything
else.
Variable precedence
Understanding Ansible's variable precedence can help you design more predictable templates. In simplified order (from lowest to highest precedence):
- Role defaults.
- Inventory variables.
- Group variables.
- Host variables.
- Playbook variables (vars).
- Playbook vars_prompt.
- Playbook vars_files.
- Role and include variables.
- Block variables.
- Task variables.
- Extra variables (always win precedence).
Best practices for variable organization
For effective template management:
- Group related variables logically.
- Use descriptive variable names.
- Separate environment-specific variables.
- Document variable purpose and acceptable values.
For example, organize your variables by role and environment:
---
# Default nginx settings
nginx_port: 80
worker_processes: auto
worker_connections: 1024
keepalive_timeout: 65
---
# Production-specific nginx settings
nginx_port: 443
ssl_enabled: true
worker_processes: 8
worker_connections: 4096
Advanced template techniques
Once you're comfortable with basic templates, you can leverage more advanced features to create sophisticated, adaptable configurations.
Complex conditionals
Jinja2 allows complex conditional logic within templates:
<VirtualHost *:{{ http_port }}>
ServerName {{ domain_name }}
{% if environment == "production" %}
{% if ssl_enabled %}
ServerAlias www.{{ domain_name }}
Redirect permanent / https://{{ domain_name }}/
{% else %}
DocumentRoot {{ prod_document_root }}
ErrorLog ${APACHE_LOG_DIR}/{{ domain_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/{{ domain_name }}_access.log combined
{% endif %}
{% elif environment == "staging" %}
DocumentRoot {{ staging_document_root }}
ErrorLog ${APACHE_LOG_DIR}/staging_{{ domain_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/staging_{{ domain_name }}_access.log combined
{% else %}
DocumentRoot {{ dev_document_root }}
ErrorLog ${APACHE_LOG_DIR}/dev_{{ domain_name }}_error.log
CustomLog ${APACHE_LOG_DIR}/dev_{{ domain_name }}_access.log combined
{% endif %}
</VirtualHost>
Here, we're using nested conditionals to create different Apache configurations based on both the environment and whether SSL is enabled.
Loops and iterations
Beyond basic for loops, Jinja2 provides several special variables within loops:
# Generated by Ansible
{% for server in dns_servers %}
nameserver {{ server }}{% if loop.first %} # Primary DNS{% endif %}
{% endfor %}
search {% for domain in search_domains %}{{ domain }}{% if not loop.last %} {% endif %}{% endfor %}
This example uses loop.first
to mark the primary DNS server and loop.last
to
properly space the search domains. Other useful loop variables include
loop.index
, loop.revindex
, and loop.length
.
For more complex iterations, we can use nested loops:
{% for app in applications %}
backend {{ app.name }}_backend
balance {{ app.balance_method | default('roundrobin') }}
{% for server in app.servers %}
server {{ server.name }} {{ server.address }}:{{ server.port }} check {% if server.backup %}backup{% endif %}
{% endfor %}
{% endfor %}
This template creates multiple backend sections in an HAProxy configuration, each with its own set of servers.
Custom filters and transformations
Ansible extends Jinja2 with many custom filters. Let's see some useful ones:
# Configuration generated on {{ ansible_date_time.date }}
app_id: {{ app_id | to_uuid }}
debug_mode: {{ debug | bool }}
server_list: {{ server_names | join(', ') }}
admin_password: {{ admin_password | password_hash('sha512') }}
timeout: {{ timeout | int }}
server_map: {{ server_map | to_json }}
Here we're using several filters:
to_uuid
to generate a UUID from a stringbool
to ensure a boolean valuejoin
to create a comma-separated listpassword_hash
to securely hash a passwordint
to convert to an integerto_json
to format a complex variable as JSON
Another powerful transformation is the default
filter, which provides a
fallback value when a variable is undefined:
global:
scrape_interval: {{ prometheus_scrape_interval | default('15s') }}
evaluation_interval: {{ prometheus_evaluation_interval | default('15s') }}
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
{% if node_exporter_enabled | default(true) %}
- job_name: 'node'
static_configs:
- targets:
{% for server in node_exporter_targets | default(['localhost:9100']) %}
- '{{ server }}'
{% endfor %}
{% endif %}
Template deployment strategies
Properly deploying templates requires understanding the various options available in the template module.
The Ansible template
module has many useful parameters:
- name: Deploy configuration templates
hosts: all
tasks:
- name: Deploy application configuration
template:
src: app_config.j2
dest: /etc/app/config.yml
owner: app_user
group: app_group
mode: '0640'
backup: yes
# This creates a backup with timestamp
# e.g., /etc/app/config.yml.2025-01-30@14:32:43~
validate: "/usr/bin/python -c 'import yaml; yaml.safe_load(open(\"%s\"))'"
lstrip_blocks: yes
trim_blocks: yes
This example demonstrates:
- Setting ownership and permissions.
- Creating backups before overwriting existing files.
- Validating the template output before deployment.
- Controlling whitespace in the rendered template.
Setting proper permissions is also crucial for security. The mode
parameter
accepts both symbolic and numeric notation:
- name: Deploy security-sensitive files
hosts: all
tasks:
- name: Deploy SSH configuration
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
validate: '/usr/sbin/sshd -t -f %s'
notify: Restart SSH service
The validate
parameter also runs a command to check the rendered file before
replacing the destination. This prevents deployment of invalid configurations:
The %s
in the validate
command is replaced with the path to a temporary file
containing the rendered template. If validation fails, the template isn't
deployed and the task fails.
Template security considerations
Security is a crucial aspect of configuration management. Here are some security considerations for Ansible templates.
Managing sensitive data
Avoid hardcoding sensitive data directly in templates. Instead:
- Use Ansible Vault for encrypting sensitive variables.
- Use environment-specific variable files that are properly secured.
- Consider using a secrets management system integrated with Ansible.
Here's an example of using encrypted variables in a template:
[database]
host={{ db_host }}
port={{ db_port | default(5432) }}
user={{ db_user }}
password={{ db_password }} # This should come from an encrypted source
The corresponding playbook might look like:
---
- name: Deploy database configuration
hosts: app_servers
vars_files:
- vars/encrypted_db_credentials.yml # Encrypted with ansible-vault
tasks:
- name: Deploy database configuration
template:
src: database.conf.j2
dest: /etc/app/database.conf
owner: app_user
group: app_group
mode: '0600' # Restrictive permissions
To create encrypted variable files:
ansible-vault create vars/encrypted_db_credentials.yml
Using Ansible Vault with templates
Ansible Vault integrates seamlessly with templates. You can encrypt entire variable files or individual variables:
# Regular variables
app_name: myapp
log_level: info
# Encrypted variables (the actual content would be encrypted)
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
31613262363835363235383066383235363661396233333433386666396136333
3530626666333333366662623339383834373833313139390a353062663333333
36636238383836353935353863656134636161396263333433386666396136333
3530626666333333366662623339383834373833313139390a353062663333333
To use encrypted variables in playbooks and templates:
ansible-playbook deploy.yml --ask-vault-pass
Or use a vault password file:
ansible-playbook deploy.yml --vault-password-file ~/.vault_pass
Final thoughts
Ansible templates transform infrastructure management through dynamic configuration generation. By organizing variables logically, implementing proper security, and leveraging roles and collections, you can build maintainable, scalable automation workflows.
Thanks for reading!
Make your mark
Join the writer's program
Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.
Write for us
Build on top of Better Stack
Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.
community@betterstack.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github