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:
Dynamic facts from target systems
Variables defined in your playbooks, inventory, and other sources
Template logic like conditionals and loops
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:
Keep Templates Readable: Add comments and organize your templates logically. What seems obvious today might be confusing months later.
Use Default Values: Always provide default values for variables that might not be defined:
{{ variable | default('fallback_value') }}
Check If Variables Exist: Use conditionals to check if variables exist before using them:
{% if some_var is defined %} {{ some_var }} {% endif %}
Use Indentation Consistently: Proper indentation makes templates much more readable:
{% if condition %} # Indented content Option value {% endif %}
Use includes for Reusable Sections: Break down complex templates into smaller, reusable parts:
{% include 'common_headers.j2' %}
Limit Logic in Templates: Keep complex logic in your playbooks or roles, and use simple logic in templates.
Test Templates Before Deployment: Use the
ansible-playbook --check
mode to verify template rendering without making changes.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