Back to Linux guides

Understanding Ansible Templates: A Comprehensive Guide

Ayooluwa Isaiah
Updated on April 6, 2025

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:

virtual-host.conf.j2
<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:

Output
<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:

nginx.conf.j2
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:

haproxy.cfg.j2
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 (|):

motd.j2
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:

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:

nginx_template_playbook.yml
- 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:

  1. Inventory files (both static and dynamic).
  2. Group variables in group_vars/ directory.
  3. Host variables in host_vars/ directory.
  4. Variables defined in playbooks.
  5. Variables defined in roles.
  6. Extra variables passed at the command line with -e or --extra-vars.
  7. Facts gathered from remote systems.

Let's see a practical example of organizing variables for a multi-environment setup:

inventory/production.ini
[web_servers]
web1.example.com
web2.example.com

[db_servers]
db1.example.com
group_vars/web_servers.yml
---
http_port: 80
enable_ssl: true
document_root: /var/www/production
max_clients: 500
host_vars/web1.example.com.yml
---
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):

  1. Role defaults.
  2. Inventory variables.
  3. Group variables.
  4. Host variables.
  5. Playbook variables (vars).
  6. Playbook vars_prompt.
  7. Playbook vars_files.
  8. Role and include variables.
  9. Block variables.
  10. Task variables.
  11. Extra variables (always win precedence).

Best practices for variable organization

For effective template management:

  1. Group related variables logically.
  2. Use descriptive variable names.
  3. Separate environment-specific variables.
  4. Document variable purpose and acceptable values.

For example, organize your variables by role and environment:

roles/nginx/defaults/main.yml
---
# Default nginx settings
nginx_port: 80
worker_processes: auto
worker_connections: 1024
keepalive_timeout: 65
environments/production/group_vars/web_servers.yml
---
# 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:

apache_vhost.conf.j2
<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:

resolv.conf.j2
# 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:

haproxy_backend.cfg.j2
{% 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:

config.yml.j2
# 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 string
  • bool to ensure a boolean value
  • join to create a comma-separated list
  • password_hash to securely hash a password
  • int to convert to an integer
  • to_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:

prometheus.yml.j2
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:

  1. Use Ansible Vault for encrypting sensitive variables.
  2. Use environment-specific variable files that are properly secured.
  3. Consider using a secrets management system integrated with Ansible.

Here's an example of using encrypted variables in a template:

database.conf.j2
[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:

secure_deploy.yml
---
- 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:

group_vars/all.yml
# 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!

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is a technical content manager at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he's not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
Common Ansible Errors and How to Fix Them
Learn how to troubleshoot and fix common Ansible errors including YAML syntax issues, connection failures, variable problems and module-specific errors to build more robust automation
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

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
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
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.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github