Jinja2 Usage in Ansible

Last updated: June 29, 2025

My Journey with Jinja2 Templates in Ansible

When I first started using Ansible, I quickly realized its power for automating system configurations. However, I also discovered that static configuration files weren't going to cut it in dynamic environments. That's when I began exploring Jinja2 templates in Ansible, and it completely changed my approach to configuration management.

In this blog post, I'll share my experience with Jinja2 templating in Ansible, demonstrating practical examples for both Linux and Windows environments. Whether you're managing a small homelab or enterprise infrastructure, mastering Jinja2 templates will elevate your automation capabilities significantly.

What is Jinja2?

Jinja2 is a modern and designer-friendly templating engine for Python. In the context of Ansible, it's the engine that powers template generation, variable expressions, and control structures in your playbooks and template files. With Jinja2, you can create dynamic configuration files that adapt based on host-specific variables, facts, and environment conditions.

The beauty of Jinja2 is that it combines the simplicity of templates with the power of programming constructs like conditionals and loops. This allows you to generate sophisticated configurations without duplicating code or resorting to complex scripting.

Jinja2 Delimiters and Syntax

Before diving into examples, let's understand the basic Jinja2 syntax used in Ansible:

  • {{ ... }} - For expressions (like variables, operations)

  • {% ... %} - For statements (like if conditions, for loops)

  • {# ... #} - For comments (won't appear in output)

  • # ... # - For line statements (alternative way to write statements)

Using Expressions with {{ ... }}

Expressions in Jinja2 are used to output values, such as variables, calculations, or function results. Here's a simple example:

The hostname of this server is {{ ansible_hostname }}.
The operating system is {{ ansible_facts['os_family'] }}.

You can also perform operations within expressions:

The total disk space is {{ ansible_facts['mounts'][0]['size_total'] / 1024 / 1024 / 1024 }} GB.
Port to use: {{ http_port + 1 }}

Using Statements with {% ... %}

Statements allow you to control the logic flow in templates with conditionals and loops:

{% if ansible_facts['os_family'] == 'Debian' %}
apt_package_manager: true
{% elif ansible_facts['os_family'] == 'RedHat' %}
yum_package_manager: true
{% else %}
unknown_package_manager: true
{% endif %}

Loops can be used to iterate through lists or dictionaries:

{% for user in users %}
  {{ user.name }} has access to {{ user.home }}
{% endfor %}

Using Comments with {# ... #}

Comments in Jinja2 templates don't appear in the final output:

{# This is a comment and won't show up in the rendered template #}
Server name: {{ server_name }}

Practical Examples for Linux and Windows

Let's look at some real-world examples of using Jinja2 templates in Ansible for both Linux and Windows environments.

Example 1: Configuring NGINX for Linux

Here's how you might create an NGINX configuration template:

# nginx.conf.j2
server {
    listen {{ nginx_port | default(80) }};
    server_name {{ server_name }};
    
    {% if ssl_enabled | default(false) %}
    listen 443 ssl;
    ssl_certificate {{ ssl_cert_path }};
    ssl_certificate_key {{ ssl_key_path }};
    {% endif %}
    
    root {{ web_root }};
    
    location / {
        try_files $uri $uri/ =404;
    }
    
    {% for location in extra_locations | default([]) %}
    location {{ location.path }} {
        {{ location.config }}
    }
    {% endfor %}
}

And the corresponding playbook:

---
- name: Configure NGINX
  hosts: webservers
  vars:
    server_name: "example.com"
    nginx_port: 8080
    ssl_enabled: true
    ssl_cert_path: "/etc/ssl/certs/example.com.crt"
    ssl_key_path: "/etc/ssl/private/example.com.key"
    web_root: "/var/www/html"
    extra_locations:
      - path: "/api"
        config: "proxy_pass http://backend:3000;"
      - path: "/media"
        config: "alias /var/www/media;"
  tasks:
    - name: Create NGINX configuration
      template:
        src: nginx.conf.j2
        dest: /etc/nginx/sites-available/example.com
        owner: root
        group: root
        mode: '0644'

Example 2: Creating a Windows IIS Configuration

For Windows environments, you might create an IIS web.config template:

<!-- web.config.j2 -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <defaultDocument>
            <files>
                {% for doc in default_documents %}
                <add value="{{ doc }}" />
                {% endfor %}
            </files>
        </defaultDocument>
        
        {% if enable_compression %}
        <urlCompression doStaticCompression="true" doDynamicCompression="true" />
        {% endif %}
        
        {% if security_headers %}
        <httpProtocol>
            <customHeaders>
                {% for header in security_headers %}
                <add name="{{ header.name }}" value="{{ header.value }}" />
                {% endfor %}
            </customHeaders>
        </httpProtocol>
        {% endif %}
        
        <handlers>
            {% if handler_type == "aspnet" %}
            <add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModuleV2" resourceType="Unspecified" />
            {% elif handler_type == "php" %}
            <add name="PHP-FastCGI" path="*.php" verb="*" modules="FastCgiModule" scriptProcessor="{{ php_processor_path }}" />
            {% endif %}
        </handlers>
    </system.webServer>
</configuration>

With the corresponding playbook:

---
- name: Configure IIS Website
  hosts: windows_webservers
  vars:
    default_documents:
      - "index.html"
      - "default.htm"
      - "Default.cshtml"
    enable_compression: true
    handler_type: "aspnet"
    security_headers:
      - name: "X-Content-Type-Options"
        value: "nosniff"
      - name: "X-Frame-Options"
        value: "SAMEORIGIN"
  tasks:
    - name: Create IIS web.config
      win_template:
        src: web.config.j2
        dest: C:\inetpub\wwwroot\web.config

Advanced Jinja2 Features

Filters in Jinja2

Jinja2 filters allow you to transform data within templates. They're applied using the pipe symbol (|). Ansible includes many built-in filters:

{{ ['web1', 'web2', 'web3'] | join(',') }}                  <!-- Results in: web1,web2,web3 -->
{{ 'MY_STRING' | lower }}                                   <!-- Results in: my_string -->
{{ some_variable | default('fallback_value') }}             <!-- Use fallback if undefined -->
{{ sensitive_data | password_hash('sha512') }}              <!-- Hash a password -->
{{ complex_object | to_json }}                              <!-- Convert to JSON -->
{{ 42 | string }}                                           <!-- Convert to string -->
{{ path_with_spaces | quote }}                              <!-- Add appropriate quotes -->

For Windows-specific work:

{{ windows_path | win_dirname }}                            <!-- Get directory from path -->
{{ windows_path | win_basename }}                           <!-- Get filename from path -->
{{ windows_path | win_splitdrive | first }}                 <!-- Get drive letter -->

Control Structures and Conditionals

Jinja2 provides several control structures that can make your templates more powerful:

{# If-elif-else statements #}
{% if environment == 'production' %}
  LogLevel warn
{% elif environment == 'development' %}
  LogLevel debug
{% else %}
  LogLevel info
{% endif %}

{# For loops with conditionals #}
{% for server in servers if server.active %}
  server {{ server.ip }}:{{ server.port }};
{% endfor %}

{# For loops with else (executed if the list is empty) #}
{% for user in users %}
  {{ user }}
{% else %}
  No users found.
{% endfor %}

{# Including other templates #}
{% include 'header.j2' %}

{# Setting variables in templates #}
{% set max_connections = 100 %}
max_connections {{ max_connections }};

Practical Templating Workflow

Here's a practical sequence diagram showing how Ansible processes Jinja2 templates:

This sequence shows why Jinja2 templating is so powerful - it combines:

  1. Dynamic facts from target systems

  2. Variables defined in your playbooks, inventory, and other sources

  3. Template logic like conditionals and loops

  4. Consistent file deployment across multiple systems

Real-world Example: Multi-Environment Configuration

Let's put everything together with a more complex example that showcases how Jinja2 can help manage configurations across different environments (development, staging, production) for both Linux and Windows servers.

Configuration Template (config.j2)

{# Common configuration for all environments #}
[General]
AppName={{ application_name }}
Version={{ application_version }}
Environment={{ environment | upper }}
LastUpdated={{ ansible_date_time.iso8601 }}

{# OS-specific configuration #}
{% if ansible_facts['os_family'] == 'Windows' %}
LogPath=C:\Logs\{{ application_name }}
DataPath=D:\Data\{{ application_name }}
{% else %}
LogPath=/var/log/{{ application_name }}
DataPath=/opt/data/{{ application_name }}
{% endif %}

{# Environment-specific settings #}
[Database]
{% if environment == 'production' %}
  {% set db_pool_size = 100 %}
  {% if ansible_facts['os_family'] == 'Windows' %}
    ConnectionString=Server={{ hostvars[groups['db_servers'][0]]['ansible_host'] }};Database={{ db_name }};Trusted_Connection=True;
  {% else %}
    ConnectionString=postgresql://{{ db_user }}:{{ db_password }}@{{ hostvars[groups['db_servers'][0]]['ansible_host'] }}/{{ db_name }}
  {% endif %}
{% elif environment == 'staging' %}
  {% set db_pool_size = 50 %}
  {% if ansible_facts['os_family'] == 'Windows' %}
    ConnectionString=Server={{ hostvars[groups['staging_db'][0]]['ansible_host'] }};Database={{ db_name }}_staging;Trusted_Connection=True;
  {% else %}
    ConnectionString=postgresql://{{ db_user }}:{{ db_password }}@{{ hostvars[groups['staging_db'][0]]['ansible_host'] }}/{{ db_name }}_staging
  {% endif %}
{% else %}
  {% set db_pool_size = 10 %}
  {% if ansible_facts['os_family'] == 'Windows' %}
    ConnectionString=Server=localhost;Database={{ db_name }}_dev;Trusted_Connection=True;
  {% else %}
    ConnectionString=postgresql://{{ db_user }}:{{ db_password }}@localhost/{{ db_name }}_dev
  {% endif %}
{% endif %}
PoolSize={{ db_pool_size }}

[Caching]
{% if environment == 'production' %}
  {% for redis_server in groups['redis_servers'] %}
RedisServer{{ loop.index }}={{ hostvars[redis_server]['ansible_host'] }}:6379
  {% endfor %}
{% else %}
RedisServer1=localhost:6379
{% endif %}

[Features]
{% for feature, enabled in features.items() %}
{{ feature }}={{ 'true' if enabled else 'false' }}
{% endfor %}

Using the Template in a Playbook

---
- name: Deploy application configuration
  hosts: app_servers
  vars:
    application_name: "MyAwesomeApp"
    application_version: "2.1.0"
    db_name: "app_database"
    db_user: "app_user"
    db_password: "{{ vault_db_password }}"  # Stored securely in Ansible vault
    features:
      Analytics: true
      UserManagement: true
      Reporting: "{{ environment == 'production' }}"
      Beta: "{{ environment != 'production' }}"
  
  tasks:
    - name: Create configuration directory (Linux)
      file:
        path: "/etc/{{ application_name }}"
        state: directory
        mode: '0755'
      when: ansible_facts['os_family'] != 'Windows'

    - name: Create configuration directory (Windows)
      win_file:
        path: "C:\\Program Files\\{{ application_name }}\\Config"
        state: directory
      when: ansible_facts['os_family'] == 'Windows'

    - name: Deploy configuration file (Linux)
      template:
        src: config.j2
        dest: "/etc/{{ application_name }}/config.ini"
        owner: "{{ app_user | default('root') }}"
        group: "{{ app_group | default('root') }}"
        mode: '0644'
      when: ansible_facts['os_family'] != 'Windows'
      notify: restart app service linux

    - name: Deploy configuration file (Windows)
      win_template:
        src: config.j2
        dest: "C:\\Program Files\\{{ application_name }}\\Config\\config.ini"
      when: ansible_facts['os_family'] == 'Windows'
      notify: restart app service windows

  handlers:
    - name: restart app service linux
      service:
        name: "{{ application_name | lower }}"
        state: restarted
      when: ansible_facts['os_family'] != 'Windows'

    - name: restart app service windows
      win_service:
        name: "{{ application_name }}Service"
        state: restarted
      when: ansible_facts['os_family'] == 'Windows'

Best Practices for Jinja2 Templates

After working extensively with Jinja2 templates in Ansible, here are some best practices I've found valuable:

  1. Keep Templates Readable: Add comments and organize your templates logically. What seems obvious today might be confusing months later.

  2. Use Default Values: Always provide default values for variables that might not be defined:

    {{ variable | default('fallback_value') }}
  3. Check If Variables Exist: Use conditionals to check if variables exist before using them:

    {% if some_var is defined %}
      {{ some_var }}
    {% endif %}
  4. Use Indentation Consistently: Proper indentation makes templates much more readable:

    {% if condition %}
      # Indented content
      Option value
    {% endif %}
  5. Use includes for Reusable Sections: Break down complex templates into smaller, reusable parts:

    {% include 'common_headers.j2' %}
  6. Limit Logic in Templates: Keep complex logic in your playbooks or roles, and use simple logic in templates.

  7. Test Templates Before Deployment: Use the ansible-playbook --check mode to verify template rendering without making changes.

  8. Document Special Variables: Add comments for any special variables or conditionals in your templates.

Conclusion

Jinja2 templating is one of the most powerful features in Ansible, allowing you to create dynamic, adaptable configurations for diverse environments. By mastering Jinja2 syntax and combining it with Ansible's inventory, facts, and variables, you can build truly intelligent automation that adapts to your infrastructure's unique needs, whether you're working with Linux, Windows, or mixed environments.

I've found that the effort invested in learning and using Jinja2 properly has saved countless hours that would otherwise be spent maintaining duplicate configurations or writing complex scripts to handle variations between environments.

In my next blog post, I'll explore Ansible magic variables and how they can further enhance your automation workflows. Stay tuned!

Last updated