Templates Jinja2

Enregistrement vidéo du cours: Lien vers la vidéo

Jinja2 est un moteur de templates pour Python utilisé par Ansible pour générer des fichiers dynamiques. Il permet de créer des fichiers de configuration personnalisés en fonction de l'environnement, de l'inventaire ou d'autres variables.

Syntaxe de base :

  • {{ variable }} : Affiche une variable
  • {% instruction %} : Structure de contrôle (if, for, etc.)
  • {# commentaire #} : Commentaire (non affiché dans le résultat)

Manipulation 2: Syntaxe de base et variables

Créer la structure du projet:

mkdir -p ansible-templates/{templates,generated}
cd ansible-templates

Créer un fichier d'inventaire hosts.ini:

[webservers]
web01 ansible_host=ADRESSE_IP
[all:vars]
ansible_user=ansible
ansible_password=Ansible123!
ansible_become_pass=Ansible123!

Créez un fichier de configuration ansible ansible.cfg:

[defaults]
inventory = hosts.ini
host_key_checking = False

Créer un template Jinja2 templates/simple.txt.j2:

{# Ceci est un commentaire - il ne sera pas affiché #}

===========================================
Configuration générée par Ansible
===========================================

Nom du serveur: {{ ansible_hostname }}
Adresse IP: {{ ansible_default_ipv4.address }}
Système d'exploitation: {{ ansible_distribution }} {{ ansible_distribution_version }}
Architecture: {{ ansible_architecture }}

Date de génération: {{ ansible_date_time.date }} à {{ ansible_date_time.time }}

{# Variables personnalisées de l'inventaire #}
Nom de domaine: {{ server_name }}
Nombre maximum de clients: {{ max_clients }}

À remarquer:

  • Les variables comme ansible_hostname, ansible_default_ipv4.address, etc., sont des faits Ansible collectés automatiquement.
  • Les variables server_name et max_clients doivent être définies dans l'inventaire ou dans le playbook.

Créer un playbook Ansible generate_config.yml:

---
- name: Test des templates Jinja2 simples
  hosts: webservers
  gather_facts: yes
  vars:
    server_name: "mon-site-exemple.com"
    max_clients: 150
  tasks:
    - name: Générer le fichier depuis le template
      template:
        src: templates/simple.txt.j2
        dest: "/tmp/{{ ansible_hostname }}_config.txt"
        mode: '0644'
    
    - name: Afficher le contenu généré
      slurp:
        src: "/tmp/{{ ansible_hostname }}_config.txt"
      register: generated_file
    
    - name: Montrer le résultat
      debug:
        msg: "{{ generated_file.content | b64decode }}"

Explications:

  • La première tâche utilise le module template pour générer un fichier de configuration à partir du template Jinja2. C'est le module clé pour travailler avec des templates.
    • Le paramètre src spécifie le chemin du template source du projet, qui contient des "placeholders", soit des endroits où les variables seront insérées.
    • Le paramètre dest spécifie le chemin de destination où le fichier généré sera sauvegardé sur le serveur cible. Lors du déploiement, les variables seront remplacées par leurs valeurs réelles. Le nom du fichier inclut le nom d'hôte à l'aide de la variable ansible_hostname. Une fois déployé, l'extension .j2 est supprimée.
    • Le paramètre mode définit les permissions du fichier généré.
  • La deuxième tâche utilise le module slurp pour lire le contenu du fichier généré, et le stocke dans la variable generated_file.
  • La troisième tâche utilise le module debug pour afficher le contenu du fichier généré. La fonction b64decode est utilisée pour décoder le contenu encodé en base64 retourné par le module slurp.

Exécutez le playbook:

ansible-playbook generate_config.yml

Après l'exécution, vous devriez voir le contenu du fichier généré affiché dans la sortie du playbook, avec les variables remplacées par leurs valeurs réelles.

Pour vérifier directement sur le serveur géré, vous pouvez vous connecter en SSH et afficher le fichier généré (modifiez ADRESSE_IP avec l'adresse IP réelle de votre serveur et node1 avec le nom d'hôte réel):

ssh ansible@ADRESSE_IP 'cat /tmp/node1_config.txt'

Filtres Jinja2

Les filtres Jinja2 permettent de manipuler les variables dans les templates. Voici ls syntaxe de base pour utiliser un filtre:

{{ variable | filtre }}

Quelques filtres courants:

  • lower : Convertit une chaîne en minuscules.
  • upper : Convertit une chaîne en majuscules.
  • default(value) : Fournit une valeur par défaut si la variable est indéfinie ou vide.
  • replace("old", "new") : Remplace les occurrences de "old" par "new".
  • length : Retourne la longueur d'une liste ou d'une chaîne.

Créez un template avec des filtres templates/filters.txt.j2:


===========================================
Démonstration des filtres Jinja2
===========================================

1. FILTRES DE CHAÎNES
---------------------
Nom du serveur (original): {{ ansible_hostname }}
Nom en majuscules: {{ ansible_hostname | upper }}
Nom en minuscules: {{ ansible_hostname | lower }}
Nom capitalisé: {{ ansible_hostname | capitalize }}
Nom avec titre: {{ ansible_hostname | title }}

2. FILTRES DE VALEURS PAR DÉFAUT
---------------------------------
Variable définie: {{ server_name | default('aucun nom') }}
Variable non définie: {{ variable_inexistante | default('valeur par défaut') }}

3. FILTRES DE REMPLACEMENT
---------------------------
Texte original: {{ ansible_hostname }}
Remplacement: {{ ansible_hostname | replace('node', 'serveur-') }} # Remplace 'node' par 'serveur-'

4. FILTRES DE LISTES
--------------------
{% set ma_liste = ['apache', 'nginx', 'mysql', 'redis'] %}
Liste originale: {{ ma_liste }}
Liste en texte avec virgules: {{ ma_liste | join(', ') }}
Liste avec trait d'union: {{ ma_liste | join(' - ') }}
Premier élément: {{ ma_liste | first }}
Dernier élément: {{ ma_liste | last }}
Nombre d'éléments: {{ ma_liste | length }}
Liste triée: {{ ma_liste | sort }}

5. FILTRES MATHÉMATIQUES
-------------------------
{% set nombre = 42 %}
Nombre: {{ nombre }}
Absolu de -42: {{ -42 | abs }}
Minimum de [10, 5, 20]: {{ [10, 5, 20] | min }}
Maximum de [10, 5, 20]: {{ [10, 5, 20] | max }}
Somme de [1, 2, 3, 4, 5]: {{ [1, 2, 3, 4, 5] | sum }}

6. FILTRES DE FORMAT
--------------------
Adresse IP: {{ ansible_default_ipv4.address }}
JSON: {{ ansible_default_ipv4 | to_json }}
YAML: {{ ansible_default_ipv4 | to_yaml }}
JSON indenté:
{{ ansible_default_ipv4 | to_nice_json }}

7. FILTRES DE DATES
-------------------
Date actuelle: {{ ansible_date_time.date }}
Timestamp: {{ ansible_date_time.epoch }}

8. FILTRES BOOLÉENS ET TESTS
-----------------------------
La variable est définie: {{ server_name is defined }}
La variable est une chaîne: {{ server_name is string }}
Le nombre est pair: {{ max_clients is even }}
Le nombre est impair: {{ max_clients is odd }}

Créez un playbook Ansible generate_filters.yml:

# Fichier: test_filters.yml
---
- name: Test des filtres Jinja2
  hosts: web01
  gather_facts: yes
  vars:
    server_name: "mon-site-exemple.com"
    max_clients: 150
  tasks:
    - name: Générer le fichier de démonstration des filtres
      template:
        src: templates/filters_demo.j2
        dest: /tmp/filters_demo.txt
        mode: '0644'
    
    - name: Récupérer et afficher le fichier
      slurp:
        src: /tmp/filters_demo.txt
      register: result
    
    - name: Afficher le résultat
      debug:
        msg: "{{ result.content | b64decode }}"

Exécutez le playbook:

ansible-playbook generate_filters.yml

Après l'exécution, vous devriez voir le contenu du fichier généré affiché dans la sortie du playbook, avec les variables transformées par les filtres Jinja2.

ssh ansible@ADRESSE_IP 'cat /tmp/filters_demo.txt'

Conditions et boucles

Jinja2 permet d'utiliser des structures de contrôle comme les conditions et les boucles pour rendre les templates plus dynamiques.

Créez un template avec conditions templates/conditions.txt.j2:

===========================================
Configuration pour {{ ansible_hostname }}
===========================================

{# Condition simple #}
{% if ansible_distribution == "Ubuntu" %}
# Configuration pour Ubuntu
apt update
apt install nginx
{% elif ansible_distribution == "CentOS" %}
# Configuration pour CentOS
yum install nginx
{% elif ansible_distribution == "Debian" %}
# Configuration pour Debian
apt-get update
apt-get install nginx
{% else %}
# Distribution non supportée
echo "Distribution {{ ansible_distribution }} non reconnue"
{% endif %}

{# Condition avec test de variable définie #}
{% if server_name is defined %}
Nom du serveur: {{ server_name }}
{% else %}
Nom du serveur: Non défini
{% endif %}

{# Conditions multiples #}
{% if max_clients is defined and max_clients > 100 %}
# Serveur haute capacité
MaxClients {{ max_clients }}
KeepAlive On
KeepAliveTimeout 5
{% else %}
# Serveur capacité standard
MaxClients 50
KeepAlive Off
{% endif %}

{# Condition avec opérateurs logiques #}
{% if ansible_memtotal_mb >= 4096 %}
memory = high
{% elif ansible_memtotal_mb >= 2048 %}
memory = medium
{% else %}
memory = low
{% endif %}

{# Test de contenu dans une liste #}
{% set supported_os = ['Ubuntu', 'Debian', 'CentOS'] %}
{% if ansible_distribution in supported_os %}
OS supporté: {{ ansible_distribution }}
{% else %}
ATTENTION: OS non supporté!
{% endif %}

{# Tests booléens #}
{% if max_clients is even %}
Le nombre de clients ({{ max_clients }}) est pair.
{% endif %}

{% if server_name is string %}
server_name est bien une chaîne de caractères.
{% endif %}

{# Condition inline (ternaire) #}
Mode: {{ 'production' if max_clients > 100 else 'développement' }}

Créez un playbook Ansible generate_conditions.yml:

---
- name: Test des conditions Jinja2
  hosts: webservers
  gather_facts: yes
  
  tasks:
    - name: Générer la configuration avec conditions
      template:
        src: templates/conditional.j2
        dest: "/tmp/{{ ansible_hostname }}_conditional.txt"
    
    - name: Afficher le résultat
      slurp:
        src: "/tmp/{{ ansible_hostname }}_conditional.txt"
      register: result
    
    - name: Montrer le contenu
      debug:
        msg: "{{ result.content | b64decode }}"

Exécutez le playbook:

ansible-playbook generate_conditions.yml

Boucles

Jinja2 permet de parcourir des listes ou des dictionnaires à l'aide de boucles for. Créez un template avec boucles templates/loops.txt.j2:

===========================================
Démonstration des boucles Jinja2
===========================================

1. BOUCLE SIMPLE SUR UNE LISTE
-------------------------------
{% set packages = ['nginx', 'php-fpm', 'mysql-server', 'redis'] %}
Packages à installer:
{% for package in packages %}
  - {{ package }}
{% endfor %}

2. BOUCLE AVEC INDEX
--------------------
{% for package in packages %}
  {{ loop.index }}. {{ package }}
{% endfor %}

3. BOUCLE AVEC INDEX COMMENÇANT À 0
------------------------------------
{% for package in packages %}
  [{{ loop.index0 }}] {{ package }}
{% endfor %}

4. VARIABLES SPÉCIALES DE BOUCLE
---------------------------------
{% for package in packages %}
Package: {{ package }}
  - Index (à partir de 1): {{ loop.index }}
  - Index (à partir de 0): {{ loop.index0 }}
  - Premier élément: {{ loop.first }}
  - Dernier élément: {{ loop.last }}
  - Nombre total d'éléments: {{ loop.length }}
  - Éléments restants: {{ loop.revindex }}
  ---
{% endfor %}

5. BOUCLE SUR UN DICTIONNAIRE
------------------------------
{% set services = {
  'web': 'nginx',
  'database': 'postgresql',
  'cache': 'redis',
  'queue': 'rabbitmq'
} %}

Services configurés:
{% for key, value in services.items() %}
  {{ key }}: {{ value }}
{% endfor %}

Créez un playbook Ansible generate_loops.yml:

# Fichier: test_loops.yml
---
- name: Test des boucles Jinja2
  hosts: web01
  gather_facts: yes
  
  tasks:
    - name: Générer le fichier de démonstration des boucles
      template:
        src: templates/loops.j2
        dest: /tmp/loops_demo.txt
    
    - name: Afficher le résultat
      slurp:
        src: /tmp/loops_demo.txt
      register: result
    
    - name: Montrer le contenu
      debug:
        msg: "{{ result.content | b64decode }}"

Exemple réaliste: Configuration Nginx

Nginx est un serveur web populaire, similaire à Apache. Nginx utilise un fichier de configuration principal (nginx.conf) et des fichiers de configuration de site individuels (souvent situés dans /etc/nginx/sites-available/). Nous allons utiliser Jinja2 pour générer ces fichiers de configuration. Créez un template pour le fichier principal templates/nginx.conf.j2:

# (1) Utilisation d'un Filtre pour transformer la chaine
# Mettre le nom du site en majuscules pour le titre du commentaire
# Filtre: | upper
# =========================================
# Configuration pour le site {{ site_name | upper }}
# =========================================

server {
    listen 80;
    
    # (2) Utilisation d'une Condition: bloc de controle {% ... %}
    {% if ssl_enabled %}
    listen 443 ssl;
    # Inclure le chemin vers les certificats uniquement si SSL est actif
    ssl_certificate /etc/nginx/ssl/{{ site_name }}.crt;
    ssl_certificate_key /etc/nginx/ssl/{{ site_name }}.key;
    {% endif %}

    server_name {{ site_name }};

    # (3) Utilisation d'une Boucle: bloc de controle {% for ... %}
    {% if users_list is defined %}
    # Ajouter des entetes pour les utilisateurs specifiques
    {% for user in users_list %}
    location /~{{ user.username }} {
        alias /var/www/users/{{ user.username }}/;
    }
    {% endfor %}
    {% endif %}
    
    # (4) Utilisation d'un autre Filtre
    # Convertir la liste d'utilisateurs en une representation YAML propre
    # Filtre: | to_nice_yaml
    # Liste des utilisateurs : 
    # {{ users_list | to_nice_yaml }}
}

Remarquons:

  • (1) Utilisation du filtre upper pour mettre en majuscules le nom du site dans le commentaire.
  • (2) Utilisation d'une condition pour inclure les directives SSL uniquement si ssl_enabled est vrai.
  • (3) Utilisation d'une boucle pour générer des blocs de configuration pour chaque utilisateur dans users_list.
  • (4) Utilisation du filtre to_nice_yaml pour afficher la liste des utilisateurs dans un format lisible.

Créez un playbook Ansible generate_nginx_config.yml:

---
- name: Generer une configuration NGINX avancée avec logique Jinja2
  hosts: linux_servers
  become: true
  
  vars:
    # Variable pour la condition (changer a false pour demontrer l'impact)
    ssl_enabled: true 
    site_name: mon-projet-securise.com
    
    # Variable pour la boucle
    users_list:
      - { username: "alice", uid: 1001 }
      - { username: "bob", uid: 1002 }
      
  tasks:
    - name: Generer le fichier de configuration
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /tmp/nginx_config.conf
        
    - name: Afficher le contenu genere pour verification
      ansible.builtin.shell: cat /tmp/nginx_config.conf
      register: advanced_config_output
      
    - name: Resultat du Template Avance
      debug:
        var: advanced_config_output.stdout_lines

Explications:

  • Le playbook définit des variables pour contrôler la génération du template, y compris une variable booléenne ssl_enabled pour la condition et une liste users_list pour la boucle.
  • La tâche utilise le module template pour générer le fichier de configuration Nginx en utilisant le template Jinja2.
  • Après la génération, le playbook affiche le contenu du fichier généré pour vérification. Exécutez le playbook:
ansible-playbook generate_nginx_config.yml

Différences entre template et copy

  • Le module template utilise Jinja2 pour traiter les fichiers source, permettant l'insertion dynamique de variables, l'utilisation de conditions et de boucles. Il est idéal pour générer des fichiers de configuration personnalisés.
  • Le module copy copie simplement un fichier statique d'une source à une destination sans traitement. Il est utilisé pour des fichiers qui ne nécessitent pas de personnalisation.

Cheat Sheet Jinja2 (feuille de référence)

Syntaxe de base Jinja2

{# Commentaire #}
{{ variable }}                    # Affichage
{% instruction %}                 # Structure de contrôle
{{ variable | filtre }}          # Application de filtre
{{ variable | filtre(arg) }}     # Filtre avec argument

Filtres les plus utilisés

{{ var | default('valeur') }}    # Valeur par défaut
{{ var | upper }}                # Majuscules
{{ var | lower }}                # Minuscules
{{ var | capitalize }}           # Première lettre en majuscule
{{ var | replace('old', 'new') }} # Remplacement
{{ liste | join(', ') }}         # Jointure
{{ liste | length }}             # Longueur
{{ liste | first }}              # Premier élément
{{ liste | last }}               # Dernier élément
{{ liste | sort }}               # Tri
{{ dict | to_json }}             # Conversion JSON
{{ dict | to_yaml }}             # Conversion YAML

Structures de contrôle

{# Conditions #}
{% if condition %}...{% endif %}
{% if cond1 %}...{% elif cond2 %}...{% else %}...{% endif %}

{# Boucles #}
{% for item in liste %}...{% endfor %}
{% for key, value in dict.items %}...{% endfor %}

{# Variables de boucle #}
{{ loop.index }}      # Index (commence à 1)
{{ loop.index0 }}     # Index (commence à 0)
{{ loop.first }}      # Premier élément?
{{ loop.last }}       # Dernier élément?
{{ loop.length }}     # Nombre total d'éléments

Whitespace control

{%- ... %}   # Supprime les espaces AVANT
{% ... -%}   # Supprime les espaces APRÈS
{%- ... -%}  # Supprime les espaces AVANT et APRÈS

Tests courants

{% if var is defined %}
{% if var is undefined %}
{% if var is none %}
{% if var is string %}
{% if var is number %}
{% if num is even %}
{% if num is odd %}
{% if item in liste %}

Accès aux variables Ansible

{{ ansible_hostname }}
{{ ansible_default_ipv4.address }}
{{ ansible_distribution }}
{{ ansible_memtotal_mb }}
{{ groups['webservers'] }}
{{ hostvars[host]['ansible_host'] }}