Using Jinja2 Templates in Ansible
Objective
This guide covers Jinja2 templates in Ansible and how to use them to generate and deploy dynamic configuration files. By the end, you will know how to:
- Create
.j2template files with Jinja2 syntax - Use variables, facts, conditionals, and loops inside templates
- Deploy templates to managed hosts using the
templatemodule - Apply host-specific values so the same template produces different configurations on different servers
- Use filters to transform variable values inside templates
- Understand how Jinja2 templates connect to RHCE exam objectives
Writing a separate configuration file for every server you manage does not scale. Jinja2 templates solve this by letting you write one file with placeholders that Ansible fills in with host-specific values at deployment time. The same template can produce a correctly configured nginx.conf for a web server with 4 cores and one for a server with 16, each tuned to its actual hardware, with no manual editing.
How Jinja2 Templates Fit Into Ansible
Ansible uses Jinja2 as its templating engine throughout: in variable definitions, in task conditions, in loop expressions, and in template files. When you use the template module, Ansible renders a .j2 file by substituting all Jinja2 expressions with their actual values, then copies the resulting file to the target host.
| Approach | How It Works | When to Use It |
|---|---|---|
| copy module | Copies a static file exactly as-is to the target host | Configuration that is identical on every host |
| template module | Renders a .j2 file with Jinja2 substitutions, copies the result |
Configuration that varies between hosts based on variables or facts |
| lineinfile / blockinfile | Modifies specific lines inside an existing file | Small targeted changes to an existing configuration |
If any value in a configuration file should differ between hosts (hostname, IP address, number of worker processes, memory limits, environment name), the template module with a .j2 file is the right tool.
Jinja2 Syntax Essentials
Jinja2 uses three types of delimiters inside template files:
| Delimiter | Purpose | Example |
|---|---|---|
{{ }} |
Output a variable or expression value | {{ ansible_hostname }} |
{% %} |
Control structures: if, for, set, block | {% if env == 'production' %} |
{# #} |
Comments (not rendered in the output file) | {# This sets the worker count #} |
Step 1: Create a Jinja2 Template File
Template files are stored in a templates/ directory inside your Ansible role or playbook directory. The filename convention is to use the target filename with a .j2 extension appended.
{# templates/nginx.conf.j2 #}
{# This template generates an nginx configuration using host variables and facts #}
user nginx;
worker_processes {{ ansible_processor_vcpus }};
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections {{ worker_connections | default(1024) }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
server_name {{ ansible_fqdn }};
sendfile on;
keepalive_timeout {{ keepalive_timeout | default(65) }};
server {
listen {{ http_port | default(80) }};
server_name {{ ansible_hostname }};
root {{ web_root | default('/var/www/html') }};
index index.html index.htm;
{# Enable gzip only in production environments #}
{% if environment == 'production' %}
gzip on;
gzip_types text/plain text/css application/json application/javascript;
{% endif %}
location / {
try_files $uri $uri/ =404;
}
}
}
What this template demonstrates:
{{ ansible_processor_vcpus }}: an Ansible fact, automatically gathered from the target host{{ worker_connections | default(1024) }}: a variable with a fallback default if not defined{{ ansible_fqdn }}and{{ ansible_hostname }}: built-in facts that are always available{% if environment == 'production' %}: a conditional block that only renders in certain environments{# comments #}: template comments that do not appear in the generated file
Step 2: Define Variables
Variables referenced in templates can come from several places. Ansible resolves them in precedence order, with more specific sources overriding more general ones:
# group_vars/webservers.yml
# Variables that apply to all hosts in the webservers group
http_port: 80
web_root: /var/www/html
environment: production
keepalive_timeout: 75
worker_connections: 2048
# host_vars/web01.example.com.yml
# Variables that apply only to this specific host
http_port: 8080
web_root: /var/www/web01
When the template is rendered for web01.example.com, host-specific values override group values. The http_port will be 8080 on that host and 80 on all others. Everything else comes from the group variables.
Step 3: Deploy the Template with the template Module
---
- name: Deploy nginx configuration
hosts: webservers
tasks:
- name: Deploy nginx configuration from template
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s
notify: Restart nginx
- name: Ensure nginx is started and enabled
ansible.builtin.service:
name: nginx
state: started
enabled: true
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Key points about the template module:
src:the path to the.j2file relative to your playbook or roledest:the full path on the target host where the rendered file is writtenvalidate:runs a validation command on the rendered file before putting it in place.%sis replaced with a temporary file path. The task fails if the validation command returns a non-zero exit code, protecting you from deploying broken configurations.notify:triggers the handler only when the file actually changes, avoiding unnecessary service restarts
Using Ansible Facts in Templates
Ansible gathers facts from each managed host before running tasks. These facts are variables you can use in templates without defining them yourself.
| Fact Variable | Example Value | Common Use in Templates |
|---|---|---|
ansible_hostname |
web01 |
Server name in configuration files |
ansible_fqdn |
web01.example.com |
Fully qualified domain name for SSL certs, virtual hosts |
ansible_default_ipv4.address |
192.168.1.10 |
Bind address for services listening on a specific interface |
ansible_processor_vcpus |
8 |
Worker process counts in nginx, Apache, PostgreSQL |
ansible_memtotal_mb |
16384 |
Memory-based settings for databases and JVM heap sizes |
ansible_os_family |
RedHat |
Conditionally include OS-specific configuration sections |
ansible_distribution_version |
10.0 |
Version-specific configuration options |
Conditionals in Templates
{# templates/app.conf.j2 #}
[server]
host = {{ ansible_default_ipv4.address }}
port = {{ app_port | default(8080) }}
{# Include SSL configuration only when certificates are defined #}
{% if ssl_cert is defined and ssl_key is defined %}
[ssl]
certificate = {{ ssl_cert }}
private_key = {{ ssl_key }}
ssl_protocols = TLSv1.2 TLSv1.3
{% endif %}
{# Different log levels per environment #}
{% if environment == 'production' %}
log_level = WARNING
{% elif environment == 'staging' %}
log_level = INFO
{% else %}
log_level = DEBUG
{% endif %}
{# Include debug settings only for non-production #}
{% if environment != 'production' %}
[debug]
enable_profiler = true
verbose_errors = true
{% endif %}
Loops in Templates
Jinja2 loops let you generate repeated configuration sections from a list variable, avoiding manual repetition for things like virtual hosts, upstream servers, or allowed IP addresses.
{# templates/haproxy.cfg.j2 #}
global
log /dev/log local0
maxconn 4096
defaults
log global
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend web_frontend
bind {{ ansible_default_ipv4.address }}:80
default_backend web_servers
backend web_servers
balance roundrobin
{# Loop over the backend_servers list variable to generate server entries #}
{% for server in backend_servers %}
server {{ server.name }} {{ server.ip }}:{{ server.port }} check
{% endfor %}
# group_vars/loadbalancers.yml
backend_servers:
- name: web01
ip: 192.168.1.10
port: 8080
- name: web02
ip: 192.168.1.11
port: 8080
- name: web03
ip: 192.168.1.12
port: 8080
The rendered haproxy.cfg will contain three server lines generated from the list, one for each entry, with the correct name, IP, and port.
Jinja2 Filters
Filters transform variable values inside a template. They are applied with a pipe character after the variable name.
{# templates/app.conf.j2 - filter examples #}
{# Convert to uppercase #}
environment = {{ environment | upper }}
{# Provide a default if the variable is not defined #}
max_connections = {{ max_connections | default(100) }}
{# Convert memory from MB to GB with rounding #}
heap_size = {{ (ansible_memtotal_mb * 0.5) | int }}m
{# Join a list into a comma-separated string #}
allowed_hosts = {{ allowed_hosts | join(', ') }}
{# Replace characters in a string #}
safe_name = {{ ansible_hostname | replace('-', '_') }}
{# Check if a variable is defined before using it #}
{% if db_password is defined %}
db_password = {{ db_password }}
{% endif %}
{# Convert a boolean to yes/no #}
ssl_enabled = {{ use_ssl | ternary('yes', 'no') }}
A Complete Practical Example
This brings all the concepts together: a PostgreSQL configuration template that tunes itself based on the host's actual memory and CPU count.
{# templates/postgresql.conf.j2 #}
{# Auto-tuned PostgreSQL configuration generated by Ansible #}
{# Host: {{ ansible_fqdn }} | Generated: {{ ansible_date_time.iso8601 }} #}
# Connection settings
listen_addresses = '{{ ansible_default_ipv4.address }}'
port = {{ pg_port | default(5432) }}
max_connections = {{ pg_max_connections | default(100) }}
# Memory settings - tuned to {{ ansible_memtotal_mb }}MB total RAM
shared_buffers = {{ (ansible_memtotal_mb * 0.25) | int }}MB
effective_cache_size = {{ (ansible_memtotal_mb * 0.75) | int }}MB
work_mem = {{ (ansible_memtotal_mb / pg_max_connections | default(100)) | int }}MB
maintenance_work_mem = {{ [((ansible_memtotal_mb * 0.05) | int), 64] | max }}MB
# CPU settings - {{ ansible_processor_vcpus }} vCPUs detected
max_worker_processes = {{ ansible_processor_vcpus }}
max_parallel_workers = {{ ansible_processor_vcpus }}
# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = '/var/log/postgresql'
log_filename = 'postgresql-%Y-%m-%d.log'
{% if environment == 'production' %}
log_min_duration_statement = 1000
log_checkpoints = on
log_connections = off
log_disconnections = off
{% else %}
log_min_duration_statement = 0
log_checkpoints = on
log_connections = on
log_disconnections = on
{% endif %}
Common Mistakes
- Using the copy module when you need template. The
copymodule does not process Jinja2 syntax. If your file contains{{ }}expressions and you usecopy, they will appear literally in the destination file instead of being substituted.- Rule of thumb: if the file has any
{{ }},{% %}, or{# #}syntax, use thetemplatemodule
- Rule of thumb: if the file has any
- Not using the validate option for critical configuration files. Deploying a broken nginx or Apache config without validation takes down the web server the moment it reloads. The
validateoption catches syntax errors before the file is written.- nginx:
validate: nginx -t -c %s - Apache:
validate: apachectl -t -f %s - visudo-managed files:
validate: visudo -cf %s
- nginx:
- Forgetting that undefined variables cause template rendering to fail. If a variable referenced in the template is not defined anywhere, Ansible raises an error. Use
| default(value)for optional variables.{{ my_var | default('fallback_value') }}renders the fallback ifmy_varis not set{{ my_var | default(omit) }}omits the line entirely if the variable is not defined
- Storing templates in the wrong location. The
templatemodule looks for.j2files relative to the playbook in atemplates/directory, or relative to the role inroles/rolename/templates/. Files placed elsewhere require an explicit path. - Putting sensitive values directly in template files. Passwords, API keys, and certificates should come from Ansible Vault-encrypted variable files, not be hardcoded into the template itself.
Quick Reference
| Task | Jinja2 Syntax |
|---|---|
| Output a variable | {{ variable_name }} |
| Output with default fallback | {{ variable_name | default('fallback') }} |
| Simple conditional | {% if condition %} ... {% endif %} |
| If/else conditional | {% if condition %} ... {% else %} ... {% endif %} |
| Loop over a list | {% for item in list %} ... {% endfor %} |
| Template comment | {# This will not appear in the output #} |
| Convert to uppercase | {{ value | upper }} |
| Join list to string | {{ list | join(', ') }} |
| Integer conversion | {{ value | int }} |
| Ternary (yes/no) | {{ condition | ternary('yes', 'no') }} |
| Check if defined | {% if variable is defined %} |
Jinja2 Templates and the RHCE Exam
Jinja2 templates are a direct RHCE exam objective. The exam tests your ability to create template files and deploy them using the template module, with configurations that vary based on host variables and Ansible facts. Common exam task patterns include:
- Create a template that uses
ansible_hostnameoransible_fqdnin the configuration - Deploy a configuration file that uses a group variable for a shared setting and a host variable to override it on a specific host
- Use a loop in a template to generate repeated configuration blocks from a list
- Use a conditional to include a section of configuration only when a variable is set to a specific value
- Combine the template module with a handler to restart the service only when the configuration actually changes
The handler pattern matters specifically because the RHCE exam environment verifies that changes are persistent and that services reflect updated configurations. A template task that changes a file but doesn't trigger a service restart leaves the running service with stale configuration.
Conclusion
Jinja2 templates are how Ansible moves from copying static files to generating configuration that is genuinely specific to each managed host. A single template file can produce correctly tuned configurations for every server in your inventory by combining host variables, group variables, and automatically gathered facts.
- Use
{{ }}for variable output,{% %}for control structures,{# #}for comments - Store templates in a
templates/directory with the.j2extension - Use the
templatemodule, notcopy, for any file containing Jinja2 expressions - Always use the
validateoption for critical service configuration files - Use
| default(value)for any variable that might not be defined on every host - Combine template tasks with handlers to restart services only when configuration actually changes
- Ansible facts like
ansible_processor_vcpusandansible_memtotal_mbmake templates genuinely self-tuning, not just variable substitution
Start the Jinja2 Templates lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-jinja2-templates