Skip to content

Jinja + Templates

Objectif

Découvrir le module template d'ansible qui permet de déployer des fichiers de configuration dynamiques à l'aide du moteur de templates Jinja2 : injection de variables, boucles, et personnalisation par hôte.

Durée

~35 minutes

Le module copy déploie un fichier tel quel, sans interprétation. Dès qu'un fichier de configuration doit contenir des valeurs qui dépendent de l'hôte cible, copy ne suffit plus : ou alors il faudrait maintenir une variante du fichier par cible.

Le module template résout ce problème en faisant passer le fichier source par le moteur Jinja2 avant de l'écrire sur la cible. Les expressions {{ variable }}, {% for %}, {% if %}… sont évaluées avec le contexte de l'hôte (variables d'inventaire, facts, vars…), exactement comme dans un playbook.

Convention templates/ + extension .j2

ansible cherche automatiquement les fichiers src: du module template dans un dossier templates/ situé dans le même répertoire que le playbook lancé (ou dans roles/<role>/templates/ pour un rôle). Par convention, les fichiers Jinja2 portent l'extension .j2 pour être identifiables d'un coup d'œil MAIS l'extension n'est pas reproduite à l'arrivée (chrony.conf.j2chrony.conf).

Mise en place du lab

On démarre les VM de l'atelier puis on se connecte au Control Host :

cd $HOME/git/gh/formation-ansible/atelier-18
vagrant up
^up^ssh ansible

# sur le control host
cd ansible/projets/ema/playbooks/

La structure du projet contient déjà un dossier templates/ avec deux templates de démonstration, et un playbook ansible-03.yml qui les déploie comme page d'accueil Apache sur chaque cible.

Structure du projet avec les fichiers template

Premier exemple : variables dans un template

Le template index.html.j2 injecte une variable ansible {{ inventory_hostname }} dans le contenu d'une page HTML. Le playbook ansible-03.yml le copie sur chaque Target Host avec le module template, puis je valide le résultat avec curl depuis le Control Host :

ansible-playbook ansible-03.yml
for target in rocky debian suse ubuntu; do curl "$target"; done

Chaque cible renvoie une page personnalisée preuve que le rendu est bien fait par hôte, à partir d'un seul fichier source.

Résultat du curl sur chaque Target Host avec les variables injectées

Deuxième exemple : boucles Jinja2

index-2.html.j2 va plus loin : il utilise une boucle {% for %} pour générer dynamiquement une liste HTML à partir d'une variable de type liste (all).

Template index-2.html.j2 avec boucle Jinja2

À vous de jouer

L'objectif est de reprendre le déploiement de Chrony du Workshop 13 qui dupliquait le fichier chrony.conf via le module copy et de le remplacer par un seul template Jinja2. Le fichier de configuration n'a pas besoin d'être différent par distribution, mais son emplacement (/etc/chrony/chrony.conf sous Debian, /etc/chrony.conf partout ailleurs...) et le nom du service (chrony vs chronyd) varient. On reprend donc l'approche include_vars du workshop précédent.

1. Création du template et des variables par famille d'OS

On crée le template chrony.conf.j2 dans templates/, ainsi que les fichiers de variables par famille d'OS (vars/chrony_debian.yml, vars/chrony_redhat.yml, vars/chrony_suse.yml). La seule expression Jinja2 utilisée ici est {{ chrony_confdir }} dans l'en-tête du fichier, à titre de marqueur, l'essentiel est que la destination dépend de la distribution.

cat > templates/chrony.conf.j2 <<'EOF'
# {{ chrony_confdir }}/chrony.conf - managed by ansible
server 0.fr.pool.ntp.org iburst
server 1.fr.pool.ntp.org iburst
server 2.fr.pool.ntp.org iburst
server 3.fr.pool.ntp.org iburst
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony
EOF

mkdir -p vars
for f in debian redhat suse; do
  [ "$f" = "debian" ] && s=chrony c=/etc/chrony || s=chronyd c=/etc
  cat > vars/chrony_${f}.yml <<EOF
---
chrony_package: chrony
chrony_service: ${s}
chrony_confdir: ${c}
EOF
done

2. Le playbook chrony.yml

Le playbook charge les variables spécifiques à l'OS, installe le paquet chrony via le module générique package, déploie la configuration avec template (qui notifie un handler de redémarrage en cas de changement), démarre le service, puis vérifie l'application de l'état instruit avec chronyc tracking et chronyc sources. L'appel explicite à meta: flush_handlers force l'exécution du handler avant les tâches de vérification, sinon chronyc interrogerait un démon qui n'a pas encore relu sa configuration...

chrony.yml
---
# chrony.yml
- name: configure chrony ntp sync with jinja2 template
  hosts: all
  become: true
  tasks:

    - name: load distribution-specific vars
      ansible.builtin.include_vars: "vars/chrony_{{ ansible_os_family | lower }}.yml"

    - name: update apt cache (debian based)
      ansible.builtin.apt:
        update_cache: true
        cache_valid_time: 3600
      when: ansible_os_family == "Debian"

    - name: install chrony
      ansible.builtin.package:
        name: "{{ chrony_package }}"
        state: present

    - name: deploy chrony.conf from jinja2 template
      ansible.builtin.template:
        src: chrony.conf.j2
        dest: "{{ chrony_confdir }}/chrony.conf"
        owner: root
        group: root
        mode: "0644"
      notify: restart chrony

    - name: start + enable chrony
      ansible.builtin.service:
        name: "{{ chrony_service }}"
        state: started
        enabled: true

    - name: flush handlers to apply restart now
      ansible.builtin.meta: flush_handlers

    - name: read deployed chrony.conf
      ansible.builtin.slurp:
        src: "{{ chrony_confdir }}/chrony.conf"
      register: chrony_conf_file

    - name: show deployed chrony.conf content
      ansible.builtin.debug:
        msg: "{{ (chrony_conf_file.content | b64decode).split('\n') }}"

    - name: wait a bit for chrony to reach sources
      ansible.builtin.pause:
        seconds: 5

    - name: check chrony tracking
      ansible.builtin.command: chronyc tracking
      register: chrony_tracking
      changed_when: false

    - name: show chrony tracking
      ansible.builtin.debug:
        var: chrony_tracking.stdout_lines

    - name: check chrony sources
      ansible.builtin.command: chronyc -n sources
      register: chrony_sources
      changed_when: false

    - name: show chrony sources
      ansible.builtin.debug:
        var: chrony_sources.stdout_lines

  handlers:
    - name: restart chrony
      ansible.builtin.service:
        name: "{{ chrony_service }}"
        state: restarted
...

3. Exécution et validation

À la première exécution, la tâche template passe en changed (le fichier est créé), le handler restart chrony est notifié puis joué via flush_handlers, et les tâches chronyc tracking et chronyc sources confirment la synchronisation effective sur chacune des quatre distributions.

Déploiement de chrony.conf via template Jinja2 sur les quatre distributions

Une seconde exécution affiche changed=0 : template compare le rendu Jinja2 au contenu déjà présent sur la cible et ne touche au fichier que s'il diffère (c'est ce qui rend le playbook idempotent malgré son apparente complexité...).

Je retiens

  • Le module template remplace copy quand le fichier doit contenir des valeurs dynamiques ({{ variable }}).
  • Les fichiers templates portent l'extension .j2 par convention et sont stockés dans un dossier templates/ MAIS l'extension n'est pas reproduite à l'arrivée.
  • Jinja2 supporte les boucles ({% for %}) et les conditions ({% if %}), ce qui permet de générer des fichiers complexes.
  • Combiné avec include_vars (cf. Workshop 13), le module template permet de personnaliser les fichiers par distribution tout en gardant un seul playbook.

Cheatsheet

Vu par expérience post-playbook.

Symptôme Cause probable Correction
template: src: chrony.conf.j2Could not find or access Le fichier n'est pas dans ./templates/ relatif au playbook/rôle Créer templates/chrony.conf.j2 dans le même répertoire que le playbook
Jinja2 : TemplateSyntaxError: expected token 'end of print statement' Balise mal fermée ({{ var } au lieu de {{ var }}) ou filtre inconnu Vérifier la syntaxe avec un linter Jinja ou ansible-playbook --check --diff
Le template génère des lignes vides en trop à cause des balises {% for %} Les balises de contrôle conservent les blancs par défaut Utiliser les variantes « strip » : {%- for x in liste -%} / {%- endfor -%}
{{ chrony_confdir }}undefined variable include_vars n'a pas été appelé avant la tâche template Placer include_vars en premier dans la liste des tâches
Les permissions du fichier déployé ne sont pas appliquées owner / group / mode absents de la tâche template Ajouter explicitement owner, group, mode (comme pour copy)
Un changement de template écrase le fichier sans sauvegarde backup absent Ajouter backup: true sur la tâche template pour garder une copie horodatée
Debug : quelle valeur a la variable dans le template ? Pas de façon simple d'inspecter en ligne Utiliser debug: var=chrony_confdir avant la tâche template, ou --check --diff pour voir le rendu

Sources (principales)


Précédent : Heterogeneous Targets