Using Jinja2 Templates in Ansible

By LinuxCert.Guru Team·

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 .j2 template files with Jinja2 syntax
  • Use variables, facts, conditionals, and loops inside templates
  • Deploy templates to managed hosts using the template module
  • 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 .j2 file relative to your playbook or role
  • dest: the full path on the target host where the rendered file is written
  • validate: runs a validation command on the rendered file before putting it in place. %s is 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 copy module does not process Jinja2 syntax. If your file contains {{ }} expressions and you use copy, they will appear literally in the destination file instead of being substituted.
    • Rule of thumb: if the file has any {{ }}, {% %}, or {# #} syntax, use the template module
  • 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 validate option 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
  • 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 if my_var is not set
    • {{ my_var | default(omit) }} omits the line entirely if the variable is not defined
  • Storing templates in the wrong location. The template module looks for .j2 files relative to the playbook in a templates/ directory, or relative to the role in roles/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_hostname or ansible_fqdn in 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 .j2 extension
  • Use the template module, not copy, for any file containing Jinja2 expressions
  • Always use the validate option 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_vcpus and ansible_memtotal_mb make templates genuinely self-tuning, not just variable substitution

Start the Jinja2 Templates lab at LinuxCert.Guru → https://linuxcert.guru/rhce/?name=rhce-jinja2-templates

$_
Ready to go beyond reading?Practice with free, auto-graded RHCSA labs.
Start free →