Ansible Template Variables
Introduction
When managing infrastructure with Ansible, you'll often need to generate configuration files that vary slightly between servers. Rather than maintaining separate files for each server, Ansible templates allow you to create a single template file and populate it with different values for each host. This is made possible through Ansible template variables.
Template variables are placeholders in your template files that Ansible replaces with actual values during playbook execution. Ansible's templating system is powered by the Jinja2 templating engine, which provides a flexible way to incorporate variables, conditionals, loops, and filters in your templates.
In this guide, we'll explore how to use variables in Ansible templates to create dynamic configuration files that adapt to your infrastructure needs.
Basic Variable Usage
The most fundamental use of variables in templates is straightforward substitution. Variables are enclosed in double curly braces {{ }}
.
Example: Basic Variable Substitution
Let's create a simple template for an NGINX server configuration:
Template file (nginx.conf.j2
):
server {
listen {{ nginx_port }};
server_name {{ server_hostname }};
root {{ web_root }};
index index.html;
}
Ansible playbook (deploy_nginx.yml
):
---
- name: Configure NGINX
hosts: webservers
vars:
nginx_port: 80
server_hostname: example.com
web_root: /var/www/html
tasks:
- name: Generate NGINX configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/conf.d/default.conf
notify:
- restart nginx
handlers:
- name: restart nginx
service:
name: nginx
state: restarted
Generated output (/etc/nginx/conf.d/default.conf
):
server {
listen 80;
server_name example.com;
root /var/www/html;
index index.html;
}
Variable Sources
Ansible template variables can come from multiple sources:
- Playbook variables: Defined directly in your playbooks with
vars:
orvars_files:
. - Inventory variables: Defined in your inventory files or in host/group vars directories.
- Facts: Information gathered automatically by Ansible about managed hosts.
- Registered variables: Values captured from task output.
- Role defaults and variables: Defined in roles structure.
- Extra variables: Passed via the command line with
-e
or--extra-vars
.
Example: Using Different Variable Sources
Inventory file (inventory.ini
):
[webservers]
web1.example.com
[webservers:vars]
web_environment=production
Host variables file (host_vars/web1.example.com.yml
):
nginx_port: 8080
server_hostname: web1.example.com
Group variables file (group_vars/webservers.yml
):
web_root: /var/www/production
Template file (nginx.conf.j2
):
# Server configuration for {{ server_hostname }}
# Environment: {{ web_environment }}
server {
listen {{ nginx_port }};
server_name {{ server_hostname }};
root {{ web_root }};
index index.html;
# Include server facts
# Managed by Ansible on {{ ansible_facts['date_time']['date'] }}
# Server OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
}
Accessing Complex Variables
Ansible variables can be complex data structures like dictionaries and lists. Jinja2 provides syntax to access these nested values.
Dictionaries
Access dictionary values using the dot notation or square brackets:
{{ user.name }} or {{ user['name'] }}
Example: Dictionary Variables
Playbook variables:
app_config:
port: 3000
log_level: info
database:
host: db.example.com
port: 5432
name: myapp
Template usage:
# Application configuration
port={{ app_config.port }}
log_level={{ app_config.log_level }}
# Database configuration
db_host={{ app_config.database.host }}
db_port={{ app_config.database.port }}
db_name={{ app_config.database.name }}
Lists
Access list items using the index:
{{ users[0] }}
Example: List Variables
Playbook variables:
allowed_hosts:
- 192.168.1.10
- 192.168.1.11
- 192.168.1.12
Template usage:
# Allowed hosts
allowed_hosts = [
{% for host in allowed_hosts %}
"{{ host }}"{% if not loop.last %},{% endif %}
{% endfor %}
]
Conditional Statements
Template variables can be used in conditional statements to include or exclude parts of your configuration based on certain conditions.
Example: Conditional Configuration
Template file (apache.conf.j2
):
<VirtualHost *:{{ apache_port | default(80) }}>
ServerName {{ server_name }}
DocumentRoot {{ document_root }}
{% if ssl_enabled %}
SSLEngine on
SSLCertificateFile {{ ssl_cert_file }}
SSLCertificateKeyFile {{ ssl_key_file }}
{% endif %}
{% if environment == "development" %}
# Development-specific settings
LogLevel debug
{% elif environment == "production" %}
# Production-specific settings
LogLevel warn
{% else %}
# Default settings
LogLevel info
{% endif %}
</VirtualHost>
Loops in Templates
You can use loops in templates to generate repetitive configuration blocks based on variables.
Example: Loop Through a List
Playbook variables:
web_apps:
- name: app1
port: 8001
path: /app1
- name: app2
port: 8002
path: /app2
- name: app3
port: 8003
path: /app3
Template file (nginx_proxy.conf.j2
):
# Proxy configuration for multiple applications
{% for app in web_apps %}
upstream {{ app.name }} {
server 127.0.0.1:{{ app.port }};
}
location {{ app.path }} {
proxy_pass http://{{ app.name }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
{% endfor %}
Filters
Jinja2 filters allow you to transform variable values before they're inserted into templates. Ansible extends the default Jinja2 filters with many additional ones.
Common Filters
default
: Provides a default value if the variable is undefinedupper
,lower
: Converts text to uppercase or lowercasejoin
: Joins list elements into a stringto_json
,to_yaml
: Converts a variable to JSON or YAML formatregex_replace
: Replaces text using regular expressions
Example: Using Filters
Template file (config.properties.j2
):
# Database configuration
db.host={{ database.host | default('localhost') }}
db.port={{ database.port | default(5432) }}
db.name={{ database.name | lower }}
# Application settings
app.environment={{ environment | upper }}
app.debug={{ debug | default(false) | bool | string }}
app.allowed_ips={{ allowed_ips | join(',') }}
# Timestamp
generated.on={{ ansible_facts.date_time.iso8601 }}
Working with Ansible Facts
Ansible facts provide a wealth of information about your target hosts that you can use in your templates.
Example: System Configuration Template
Template file (system_info.conf.j2
):
# System Information for {{ ansible_facts['hostname'] }}
# Generated by Ansible on {{ ansible_facts['date_time']['date'] }}
OS: {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}
Architecture: {{ ansible_facts['architecture'] }}
Kernel: {{ ansible_facts['kernel'] }}
# Memory Information
Total Memory: {{ (ansible_facts['memtotal_mb'] / 1024) | round(2) }} GB
Swap Space: {{ (ansible_facts['swaptotal_mb'] / 1024) | round(2) }} GB
# Network Information
{% for interface, data in ansible_facts['interfaces'] | dict2items %}
{% if interface != 'lo' %}
Interface {{ interface }}:
IP Address: {{ data.ipv4.address | default('Not configured') }}
Netmask: {{ data.ipv4.netmask | default('Not configured') }}
{% endif %}
{% endfor %}
Combining Variables with Logical Operations
Jinja2 allows for complex logical operations with variables.
Example: Advanced Conditional Logic
Template file (security.conf.j2
):
# Security configuration
{% if environment == "production" and enforce_ssl | bool %}
require_ssl = true
ssl_min_version = TLSv1.2
{% elif environment == "staging" and enforce_ssl | bool %}
require_ssl = true
ssl_min_version = TLSv1.1
{% else %}
require_ssl = false
{% endif %}
# IP restrictions
{% if restricted_access | default(false) | bool %}
allowed_networks = {{ allowed_networks | join(',') }}
{% if additional_networks is defined and additional_networks | length > 0 %}
additional_allowed = {{ additional_networks | join(',') }}
{% endif %}
{% else %}
# No IP restrictions applied
{% endif %}
Practical Example: Creating a Multi-Environment Configuration
Let's create a practical example for a web application that requires different configurations based on the environment.
Directory Structure
ansible/
├── inventory/
│ ├── production
│ └── staging
├── group_vars/
│ ├── all.yml
│ ├── production.yml
│ └── staging.yml
├── templates/
│ └── app_config.json.j2
└── deploy_app.yml
Variables Definition
group_vars/all.yml:
app_name: mywebapp
app_port: 3000
log_format: json
group_vars/production.yml:
environment: production
debug_mode: false
database:
host: prod-db.example.com
port: 5432
user: app_user
name: app_production
cache:
enabled: true
ttl: 3600
group_vars/staging.yml:
environment: staging
debug_mode: true
database:
host: staging-db.example.com
port: 5432
user: app_user
name: app_staging
cache:
enabled: false
ttl: 60
Template
templates/app_config.json.j2:
{
"appName": "{{ app_name }}",
"environment": "{{ environment }}",
"port": {{ app_port }},
"debug": {{ debug_mode | bool | lower }},
"logConfig": {
"format": "{{ log_format }}",
"level": "{{ 'debug' if debug_mode else 'info' }}"
},
"database": {
"host": "{{ database.host }}",
"port": {{ database.port }},
"user": "{{ database.user }}",
"name": "{{ database.name }}"
},
"cache": {
"enabled": {{ cache.enabled | bool | lower }},
{% if cache.enabled %}
"ttl": {{ cache.ttl }},
"provider": "{{ 'redis' if environment == 'production' else 'memory' }}"
{% else %}
"provider": "none"
{% endif %}
},
"generatedBy": "Ansible on {{ ansible_facts.date_time.iso8601 }}"
}
Playbook
deploy_app.yml:
---
- name: Deploy application configuration
hosts: all
tasks:
- name: Create application directory
file:
path: "/opt/{{ app_name }}/config"
state: directory
mode: '0755'
- name: Generate application config
template:
src: templates/app_config.json.j2
dest: "/opt/{{ app_name }}/config/config.json"
mode: '0644'
notify:
- restart application
handlers:
- name: restart application
service:
name: "{{ app_name }}"
state: restarted
Template Flow Control Diagram
Here's a visual representation of how template variables are processed:
Best Practices for Template Variables
-
Use meaningful variable names: Choose descriptive names that indicate the purpose of the variable.
-
Organize variables hierarchically: Group related variables in dictionaries to keep them organized.
-
Provide default values: Use the
default
filter to ensure templates work even if a variable is not defined.jinja2{{ app_port | default(8080) }}
-
Use the appropriate data types: Boolean values should be boolean, numbers should be numeric.
-
Escape special characters: Use quotes for strings containing special characters in YAML.
-
Comment your templates: Add comments to explain complex logic or variable usage.
-
Use environment-specific variables: Structure your variables based on environments.
-
Validate variables: Use Ansible's
assert
module to validate variable values before applying templates.yaml- name: Validate variables
assert:
that:
- app_port is defined and app_port | int > 0
- app_name is defined and app_name | length > 0
fail_msg: "Required variables are missing or invalid"
Summary
Ansible template variables provide a powerful mechanism for creating dynamic configuration files that adapt to your infrastructure needs. By leveraging Jinja2's templating capabilities, you can:
- Substitute variables with actual values
- Apply conditionals to include or exclude configuration sections
- Use loops to generate repetitive configuration blocks
- Transform values using filters
- Access complex data structures
- Utilize system facts for host-specific configurations
This approach allows you to maintain a single template file instead of multiple static configuration files, significantly reducing maintenance overhead and ensuring consistency across your infrastructure.
Exercises
-
Create a template for an Apache virtual host configuration that supports both HTTP and HTTPS based on variables.
-
Design a template for a database configuration file that supports multiple database types (MySQL, PostgreSQL) with appropriate settings for each.
-
Build a template for a load balancer configuration that dynamically includes backend servers from an Ansible inventory.
-
Create a template that generates different logging configurations based on the environment (development, staging, production).
-
Design a template for a firewall configuration that includes rules from a list of dictionaries containing service names, ports, and allowed IP ranges.
Additional Resources
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)