Jinja2 templates
Chapter objectives
- Understand the Jinja2 syntax
- Create dynamic templates
- Use filters and tests
- Handle complex structures
1 - Introduction to templates
Why templates?
The template module
tasks:
- name: Générer la configuration nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
backup: yes
validate: nginx -t -c %s
2 - Jinja2 syntax
Three types of tags
{# Ceci est un commentaire - ignoré #}
{{ variable }} {# Affiche une variable #}
{% if condition %} {# Structure de contrôle #}
{% endif %}
Simple variables
# nginx.conf.j2
server {
listen {{ http_port }};
server_name {{ server_name }};
root {{ document_root }};
}
With the variables:
http_port: 80
server_name: example.com
document_root: /var/www/html
Result:
server {
listen 80;
server_name example.com;
root /var/www/html;
}
Dictionaries
# database.conf.j2
[database]
host = {{ database.host }}
port = {{ database.port }}
name = {{ database.name }}
user = {{ database.user }}
password = {{ database.password }}
database:
host: localhost
port: 5432
name: myapp
user: admin
password: secret
3 - Control structures
Conditions (if)
{% if ssl_enabled %}
server {
listen 443 ssl;
ssl_certificate {{ ssl_cert }};
ssl_certificate_key {{ ssl_key }};
}
{% else %}
server {
listen 80;
}
{% endif %}
Conditions with elif
{% if environment == 'production' %}
log_level = warn
{% elif environment == 'staging' %}
log_level = info
{% else %}
log_level = debug
{% endif %}
Loops (for)
# /etc/hosts
127.0.0.1 localhost
{% for host in web_servers %}
{{ host.ip }} {{ host.name }}
{% endfor %}
web_servers:
- { name: web-01, ip: "192.168.1.10" }
- { name: web-02, ip: "192.168.1.11" }
- { name: web-03, ip: "192.168.1.12" }
Result:
127.0.0.1 localhost
192.168.1.10 web-01
192.168.1.11 web-02
192.168.1.12 web-03
Loops with index
{% for server in upstream_servers %}
server {{ server }} weight={{ loop.index }};
{% endfor %}
| Variable | Description |
|---|---|
loop.index | Index (starts at 1) |
loop.index0 | Index (starts at 0) |
loop.first | True if first element |
loop.last | True if last element |
loop.length | Number of elements |
{% for user in users %}
{{ user.name }}{% if not loop.last %}, {% endif %}
{% endfor %}
{# Résultat: alice, bob, charlie #}
4 - Filters
Text filters
{{ "hello" | upper }} {# HELLO #}
{{ "HELLO" | lower }} {# hello #}
{{ "hello world" | title }} {# Hello World #}
{{ "hello world" | capitalize }} {# Hello world #}
{{ " hello " | trim }} {# hello #}
{{ "hello" | replace("l", "x") }} {# hexxo #}
Default value filters
{{ undefined_var | default("valeur_defaut") }}
{{ empty_var | default("fallback", true) }}
{{ my_var | default(omit) }} {# Omet la ligne si undefined #}
Numeric filters
{{ 3.7 | round }} {# 4 #}
{{ 3.7 | round(1) }} {# 3.7 #}
{{ 3.7 | int }} {# 3 #}
{{ "42" | int }} {# 42 #}
{{ 1024 | human_readable }} {# 1 KB #}
List filters
{{ [1, 2, 3] | length }} {# 3 #}
{{ [1, 2, 3] | first }} {# 1 #}
{{ [1, 2, 3] | last }} {# 3 #}
{{ [3, 1, 2] | sort }} {# [1, 2, 3] #}
{{ [1, 2, 2, 3] | unique }} {# [1, 2, 3] #}
{{ [1, 2] + [3, 4] }} {# [1, 2, 3, 4] #}
{{ [1, 2, 3] | join(", ") }} {# 1, 2, 3 #}
{{ [1, 2, 3] | random }} {# élément aléatoire #}
JSON/YAML filters
{{ my_dict | to_json }}
{{ my_dict | to_nice_json }}
{{ my_dict | to_yaml }}
{{ my_dict | to_nice_yaml }}
Hash filters
{{ "password" | hash('sha256') }}
{{ "password" | password_hash('sha512') }}
{{ "hello" | b64encode }}
{{ "aGVsbG8=" | b64decode }}
5 - Tests
Test syntax
{% if variable is defined %}
{% if variable is not defined %}
{% if variable is none %}
{% if number is even %}
{% if number is odd %}
{% if value is string %}
{% if value is number %}
{% if path is file %}
{% if path is directory %}
{% if name is match("^web-") %}
{% if name is search("pattern") %}
Practical examples
{% if nginx_ssl_cert is defined and nginx_ssl_cert %}
ssl_certificate {{ nginx_ssl_cert }};
{% endif %}
{% if ansible_os_family is match("Debian|Ubuntu") %}
# Configuration Debian
{% endif %}
6 - Advanced examples
Complete nginx configuration
# {{ ansible_managed }}
# Generated on {{ ansible_date_time.iso8601 }}
user {{ nginx_user | default('www-data') }};
worker_processes {{ nginx_worker_processes | default('auto') }};
pid /run/nginx.pid;
events {
worker_connections {{ nginx_worker_connections | default(1024) }};
{% if nginx_use_epoll | default(true) %}
use epoll;
{% endif %}
}
http {
sendfile on;
tcp_nopush on;
types_hash_max_size 2048;
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
access_log {{ nginx_access_log | default('/var/log/nginx/access.log') }};
error_log {{ nginx_error_log | default('/var/log/nginx/error.log') }};
# Gzip
{% if nginx_gzip_enabled | default(true) %}
gzip on;
gzip_types text/plain text/css application/json application/javascript;
{% endif %}
# Upstream
{% for upstream in nginx_upstreams | default([]) %}
upstream {{ upstream.name }} {
{% for server in upstream.servers %}
server {{ server.address }}:{{ server.port | default(80) }}{% if server.weight is defined %} weight={{ server.weight }}{% endif %};
{% endfor %}
}
{% endfor %}
# Virtual hosts
{% for vhost in nginx_vhosts | default([]) %}
server {
listen {{ vhost.port | default(80) }}{% if vhost.ssl | default(false) %} ssl{% endif %};
server_name {{ vhost.server_name | join(' ') if vhost.server_name is iterable and vhost.server_name is not string else vhost.server_name }};
root {{ vhost.root }};
{% if vhost.ssl | default(false) %}
ssl_certificate {{ vhost.ssl_certificate }};
ssl_certificate_key {{ vhost.ssl_certificate_key }};
{% endif %}
{% for location in vhost.locations | default([]) %}
location {{ location.path }} {
{% if location.proxy_pass is defined %}
proxy_pass {{ location.proxy_pass }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
{% else %}
try_files $uri $uri/ =404;
{% endif %}
}
{% endfor %}
}
{% endfor %}
}
PostgreSQL configuration
# {{ ansible_managed }}
# PostgreSQL configuration
# Connection Settings
listen_addresses = '{{ postgresql_listen_addresses | default("localhost") }}'
port = {{ postgresql_port | default(5432) }}
max_connections = {{ postgresql_max_connections | default(100) }}
# Memory
shared_buffers = {{ (ansible_memtotal_mb * 0.25) | int }}MB
effective_cache_size = {{ (ansible_memtotal_mb * 0.75) | int }}MB
work_mem = {{ postgresql_work_mem | default('4MB') }}
maintenance_work_mem = {{ postgresql_maintenance_work_mem | default('64MB') }}
# WAL
wal_level = {{ postgresql_wal_level | default('replica') }}
max_wal_senders = {{ postgresql_max_wal_senders | default(3) }}
# Logging
log_destination = 'stderr'
logging_collector = on
log_directory = '{{ postgresql_log_directory | default("/var/log/postgresql") }}'
log_filename = 'postgresql-%Y-%m-%d.log'
log_rotation_age = 1d
log_min_duration_statement = {{ postgresql_log_min_duration | default(1000) }}
{% if postgresql_hba_entries is defined %}
# pg_hba.conf entries will be configured separately
{% endif %}
7 - Macros
Define a macro
{% macro nginx_server(name, port=80, ssl=false) %}
server {
listen {{ port }}{% if ssl %} ssl{% endif %};
server_name {{ name }};
{% if ssl %}
ssl_certificate /etc/ssl/certs/{{ name }}.crt;
ssl_certificate_key /etc/ssl/private/{{ name }}.key;
{% endif %}
}
{% endmacro %}
Use the macro
{{ nginx_server('example.com') }}
{{ nginx_server('secure.example.com', 443, true) }}
8 - Whitespace control
Problem
{% for item in items %}
{{ item }}
{% endfor %}
Generates empty lines.
Solution
{%- for item in items %}
{{ item }}
{%- endfor %}
{# Le - supprime les espaces/newlines #}
| Syntax | Effect |
|---|---|
{%- | Removes whitespace before |
-%} | Removes whitespace after |
{{- | Removes whitespace before |
-}} | Removes whitespace after |
9 - Best practices
File header
# {{ ansible_managed }}
# Do not edit manually - changes will be overwritten
# Template: {{ template_path }}
# Generated: {{ ansible_date_time.iso8601 }}
# Host: {{ inventory_hostname }}
Validation
tasks:
- name: Generate nginx config
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
validate: nginx -t -c %s
notify: Reload nginx
Backup
tasks:
- name: Generate config with backup
template:
src: app.conf.j2
dest: /etc/app/app.conf
backup: yes
10 - Template structure
Include other templates
# nginx.conf.j2
http {
{% include 'includes/ssl.conf.j2' %}
{% include 'includes/security.conf.j2' %}
}
Summary
Key points
- Use
{{ ansible_managed }}in the header - Leverage filters to transform data
- Always test with
validatewhen possible - Manage whitespace with
{%-and-%}
Practical exercises
- Create an nginx template with dynamic vhosts
- Generate an /etc/hosts file from the inventory
- Use conditions for different OSes
- Create a reusable macro