Aller au contenu principal
AnsibleDevOpsJinja2AutomationInfrastructure as Code

Templates Jinja2 avancés

30 min de lecture Ansible — Chapitre 6

Templates Jinja2 pour générer des configurations dynamiques : filtres, boucles, conditions, macros et exercices pratiques complets.

Jusqu’ici, tu as tes playbooks, tes variables, ton Vault pour les secrets. Mais dès que tu dois générer un fichier de config — Nginx, systemd, .env — tu te retrouves à copier des fichiers statiques et les modifier à la main. C’est exactement le genre de tâche répétitive qu’Ansible peut automatiser. La solution : les templates Jinja2, le moteur de templating intégré à Ansible qui transforme des squelettes de fichiers en configurations dynamiques adaptées à chaque serveur.

Dans ce chapitre, tu vas apprendre à écrire des templates Jinja2 qui génèrent automatiquement des fichiers de configuration propres, maintenables et adaptés à chaque environnement.

Pourquoi les templates changent tout

Sans templates, tu te retrouves avec un fichier nginx.conf par serveur. Trois environnements ? Trois fichiers quasi identiques à maintenir. Dix serveurs ? Bonne chance.

Les templates Jinja2 résolvent ce problème en séparant la structure (le template) des données (les variables). Un seul fichier .j2 génère autant de configurations que nécessaire — prod, staging, dev — sans duplication.

🔥 Cas réel : Une équipe infra maintenait 47 fichiers de config Nginx à la main. Après migration vers des templates Jinja2, ils sont passés à 3 templates et un fichier de variables par environnement. Résultat : les erreurs de config ont chuté de 80% et les déploiements sont passés de 45 minutes à 5 minutes.

💡 Tip DevOps : Ajoute toujours un commentaire en haut de tes fichiers générés : # Managed by Ansible - DO NOT EDIT. Ça évitera qu’un collègue modifie le fichier à la main et se fasse écraser au prochain déploiement.

Comprendre Jinja2 : la syntaxe clé

Jinja2 utilise trois types de délimiteurs que tu vas rencontrer partout :

  • {{ variable }} — affiche une valeur
  • {% if ... %} / {% for ... %} — logique de contrôle
  • {# commentaire #} — commentaire (ignoré à la génération)

Voici un exemple concret qui combine les trois. Ce template génère un bloc de configuration dont le contenu change selon l’environnement cible :

{# Template de log Nginx par environnement #}
{% if env == 'production' %}
error_log /var/log/nginx/error.log warn;
{% elif env == 'staging' %}
error_log /var/log/nginx/error.log info;
{% else %}
error_log /var/log/nginx/error.log debug;
{% endif %}

{% for server in upstream_servers %}
server {{ server }}:{{ app_port | default(3000) }};
{% endfor %}

Les boucles {% for %} itèrent sur des listes ou dictionnaires. Tu as accès à des variables de boucle utiles : loop.index (compteur à partir de 1), loop.first, loop.last, et loop.length.

Les conditions {% if %} testent des valeurs, et tu peux vérifier si une variable existe avec is defined — indispensable pour gérer les options facultatives.

🧠 À retenir : Jinja2 est évalué côté contrôleur (ta machine Ansible), pas sur le serveur cible. Le fichier envoyé est du texte pur — le serveur distant ne voit jamais de syntaxe Jinja2.

Les filtres et macros essentiels

Les filtres transforment les données à la volée avec le pipe |. Voici ceux que tu utiliseras quotidiennement dans tes templates. Le premier bloc couvre la manipulation de texte et de listes :

{# Valeur par défaut si variable non définie #}
listen {{ nginx_port | default(80) }};

{# Joindre une liste en chaîne #}
gzip_types {{ gzip_types | join(' ') }};

{# Conversion JSON pour fichier de config #}
config_data={{ app_config | to_nice_json(indent=2) }}

{# Hash de mot de passe pour /etc/shadow #}
{{ user_password | password_hash('sha512') }}

⚠️ Attention : Le filtre default() ne se déclenche que si la variable est undefined. Une variable définie à "" (chaîne vide) ou false ne sera PAS remplacée par la valeur par défaut. Si tu veux aussi couvrir les valeurs falsy, utilise default('fallback', true).

Pour éviter de répéter du code, Jinja2 propose les macros — des fonctions réutilisables directement dans ton template. Définis une macro une fois, puis appelle-la autant de fois que nécessaire :

{% macro server_block(name, port, root) %}
server {
    listen {{ port }};
    server_name {{ name }};
    root {{ root }};
    location / {
        try_files $uri $uri/ =404;
    }
}
{% endmacro %}

{{ server_block('devopslab.ch', 80, '/var/www/devopslab') }}
{{ server_block('api.devopslab.ch', 8080, '/var/www/api') }}

💡 Tip DevOps : Les macros brillent quand tu as des patterns répétitifs (blocs location Nginx, blocs upstream, entrées iptables). Un template avec 3 macros bien pensées remplace souvent 200 lignes de copier-coller.

Cas concret : Nginx multi-environnement en entreprise

Imaginons que tu gères une app Node.js déployée sur plusieurs serveurs avec un load balancer Nginx devant. Chaque serveur a un nombre d’instances différent, et le SSL n’est activé qu’en prod. Voici les variables que tu définis dans group_vars :

# group_vars/production.yml
app_name: devopslab-api
app_port_start: 3000
app_instances: 4
nginx_server_name: api.devopslab.ch
nginx_ssl: true
nginx_security_headers: true

Et voici le template Nginx qui s’adapte automatiquement au contexte. Il génère le bloc upstream avec le bon nombre d’instances, active le SSL et les headers de sécurité uniquement quand demandé :

# {{ app_name }} - Managed by Ansible
# Generated: {{ ansible_date_time.iso8601 }}

upstream {{ app_name }}_backend {
{% for i in range(0, app_instances) %}
    server 127.0.0.1:{{ app_port_start + i }};
{% endfor %}
    keepalive 32;
}

{% if nginx_ssl | default(false) %}
server {
    listen 80;
    server_name {{ nginx_server_name }};
    return 301 https://$server_name$request_uri;
}
{% endif %}

server {
{% if nginx_ssl | default(false) %}
    listen 443 ssl http2;
    ssl_certificate /etc/letsencrypt/live/{{ nginx_server_name }}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/{{ nginx_server_name }}/privkey.pem;
{% else %}
    listen 80;
{% endif %}
    server_name {{ nginx_server_name }};

{% if nginx_security_headers | default(false) %}
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000" always;
{% endif %}

    location / {
        proxy_pass http://{{ app_name }}_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Le playbook qui déploie ce template utilise le module template d’Ansible. Il pousse le fichier généré et recharge Nginx uniquement si le fichier a changé grâce au handler :

- name: Déployer la config Nginx
  ansible.builtin.template:
    src: nginx-app.conf.j2
    dest: "/etc/nginx/sites-available/{{ app_name }}.conf"
    owner: root
    mode: "0644"
    validate: "nginx -t -c %s"
  notify: Reload nginx

🔥 Cas réel : Le paramètre validate est un game-changer. Il exécute nginx -t sur le fichier généré avant de le mettre en place. Si la syntaxe est invalide, Ansible refuse le déploiement — ton Nginx en production reste intact.

Pièges fréquents

Les espaces parasites. Par défaut, Jinja2 conserve tous les espaces et sauts de ligne autour des blocs {% %}. Résultat : des lignes vides partout dans ton fichier généré. Utilise le tiret - pour les supprimer :

{# Sans tiret → lignes vides entre chaque entrée #}
{% for server in servers %}
server {{ server }};
{% endfor %}

{# Avec tiret → résultat propre, sans lignes vides #}
{% for server in servers -%}
server {{ server }};
{% endfor -%}

Variable undefined vs variable vide. Tu testes {% if ssl_cert is defined %} mais la variable est définie à "" — le bloc s’exécute quand même. Teste plutôt {% if ssl_cert is defined and ssl_cert %} pour couvrir les deux cas.

Oublier | mandatory. Si une variable critique manque, Ansible continue silencieusement avec une valeur vide. Utilise {{ db_password | mandatory }} pour forcer une erreur explicite plutôt qu’un déploiement cassé.

⚠️ Attention : Ne mets jamais de logique métier complexe dans tes templates. Si tu as plus de 3 niveaux d’imbrication if/for, c’est un signe que la logique devrait être dans tes variables ou tes tasks, pas dans le template.

Exercice : déploie ta config dynamique

Mets en pratique ce que tu viens d’apprendre avec cet exercice complet :

Objectif : Crée un template Jinja2 pour un fichier /etc/hosts dynamique.

  1. Définis une liste de serveurs dans group_vars/all.yml avec pour chaque entrée : name, ip, et role (master ou worker)
  2. Écris un template hosts.j2 qui itère sur cette liste avec {% for %}
  3. Ajoute un commentaire # MASTER à côté des serveurs dont le rôle est master ({% if %})
  4. Utilise le filtre | upper pour mettre le nom en majuscules dans le commentaire
  5. Déploie avec un playbook utilisant le module ansible.builtin.template

Résultat attendu :

# Managed by Ansible
192.168.1.10  db01       # MASTER - DB01
192.168.1.11  db02
192.168.1.20  web01      # MASTER - WEB01
192.168.1.21  web02

Bonus : Ajoute validate: "python3 -c 'open(\"%s\").read()'" pour vérifier que le fichier est lisible avant déploiement.


📝 À retenir

  • {{ }} affiche, {% %} contrôle, {# #} commente — les trois piliers de Jinja2
  • Les filtres (default, join, mandatory) transforment tes données à la volée
  • Les macros éliminent la duplication dans les templates complexes
  • Toujours utiliser validate dans le module template pour éviter de casser la prod
  • Un template bien conçu + des variables organisées = zéro fichier de config dupliqué
  • Le tiret -%} est ton allié contre les lignes vides parasites

🧠 À retenir : Un bon template Jinja2 est comme une bonne fonction — il fait une chose, il la fait bien, et il est facile à lire. Si ton template dépasse 80 lignes, découpe-le en plusieurs fichiers avec {% include %}.

➡️ Prochain chapitre : le déploiement avancé avec Ansible — stratégies rolling update, gestion des erreurs et intégration CI/CD.

🖥️ Pratique sur ton propre serveur

Pour suivre Ansible en conditions réelles, tu as besoin d'un VPS. DigitalOcean offre 200$ de crédit gratuit pour démarrer.

Obtenir 200$

Articles liés