# 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.

[ad-logs]

## Understanding Jinja2 template engine basics

Ansible templates use [Jinja2](https://jinja.palletsprojects.com/), 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:

```text
[label 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:

```text
[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:

```text
[label 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:

```text
[label 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 (`|`):

```text
[label 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`:

```text
[label 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, group_vars, host_vars, or
directly in the playbook. Here's an example using variables in a playbook:

```yaml
[label 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:

```command
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:

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

[db_servers]
db1.example.com
```

```text
[label group_vars/web_servers.yml]
---
http_port: 80
enable_ssl: true
document_root: /var/www/production
max_clients: 500
```

```text
[label 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 host_vars override those in
group_vars, 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:

```text
[label roles/nginx/defaults/main.yml]
---
# Default nginx settings
nginx_port: 80
worker_processes: auto
worker_connections: 1024
keepalive_timeout: 65
```

```text
[label 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:

```text
[label 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:

```text
[label 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:

```text
[label 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:

```text
[label 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:

```text
[label 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:

```yaml
- name: Deploy configuration templates
  hosts: all
  tasks:
    - name: Deploy application configuration
[highlight]
      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
[/highlight]
```

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:

```yaml
- 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
[highlight]
        mode: '0600'
        validate: '/usr/sbin/sshd -t -f %s'
[/highlight]
      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](https://docs.ansible.com/ansible/latest/vault_guide/index.html)
   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:

```text
[label 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:

```text
[label 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:

```command
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:

```text
[label 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:

```command
ansible-playbook deploy.yml --ask-vault-pass
```

Or use a vault password file:

```command
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!
