Chapitre 718 minIntermédiaire
Templates Jinja2 et fichiers de configuration
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- écrire un template Jinja2 avec variables, boucles et conditions ;
- transformer des valeurs avec les filtres ;
- déployer un fichier avec
templateet le valider avant de l'installer ; - choisir entre
template,copy,lineinfileetblockinfile.
4 exercices corrigés1 schéma
Un template Ansible est un fichier de configuration modèle, écrit avec le langage Jinja2, dans lequel des variables, des boucles et des conditions sont remplacées par les valeurs de chaque hôte au moment du déploiement. Le module template produit ainsi, à partir d'un seul modèle, le bon fichier pour chaque serveur. Ce chapitre présente la syntaxe Jinja2, les filtres utiles, le module template et sa validation, et dit quand préférer copy, lineinfile ou blockinfile.
Le principe
Un fichier de configuration est rarement identique d'un serveur à l'autre : le nom du site change, la liste des serveurs DNS dépend du site, une option n'est utile que sur certains hôtes. Le template garde ce qui est commun et remplace ce qui varie par des variables.
Le rendu a lieu sur le nœud de contrôle ; seul le fichier final est envoyé. Le module compare ensuite ce fichier à celui du serveur : s'ils sont identiques, rien ne change (ok) ; sinon, il remplace le fichier (changed) et notifie les handlers. L'idempotence est donc assurée par la comparaison.
La syntaxe Jinja2
Jinja2 utilise trois délimiteurs :
| Délimiteur | Rôle | Exemple |
|---|---|---|
{{ … }} | Afficher une valeur | ServerName {{ web_nom_site }} |
{% … %} | Une instruction : boucle, condition | {% for s in serveurs_ntp %} … {% endfor %} |
{# … #} | Un commentaire, absent du fichier final | {# liste fournie par group_vars/all.yml #} |
Le modèle roles/socle/templates/chrony.conf.j2 de Sorbier :
# {{ ansible_managed }}
{% for serveur in serveurs_ntp %}
server {{ serveur }} iburst
{% endfor %}
driftfile /var/lib/chrony/chrony.drift
makestep 1 3
rtcsync
logdir /var/log/chrony
Rendu pour srv-journaux, avec serveurs_ntp: [192.168.10.177, 192.168.10.181] :
# Ansible managed
server 192.168.10.177 iburst
server 192.168.10.181 iburst
driftfile /var/lib/chrony/chrony.drift
makestep 1 3
rtcsync
logdir /var/log/chrony
ansible_managed est une variable fournie par Ansible, qui vaut « Ansible managed » par défaut. La mettre en commentaire en tête de chaque fichier prévient celui qui ouvre le fichier sur le serveur : une modification à la main sera écrasée au prochain passage.
Les lignes {% for %} et {% endfor %} ne laissent pas de ligne vide : le module template active l'option Jinja2 trim_blocks, qui supprime le retour à la ligne qui suit une instruction. Pour supprimer aussi l'espace avant une instruction, ajoute un tiret : {%- if … %}.
Les conditions s'écrivent avec if, elif, else, endif :
{% if web_hsts | default(true) %}
Header always set Strict-Transport-Security "max-age=31536000"
{% endif %}
Les boucles parcourent une liste ou, avec .items(), un dictionnaire. La boucle a des variables utiles : loop.index (numéro du tour, à partir de 1), loop.first, loop.last.
Les autres hôtes sont accessibles par groups et hostvars. Ce modèle de fichier /etc/hosts liste tous les serveurs Linux de l'inventaire, utile pour un serveur de la DMZ qui n'interroge pas le DNS interne :
127.0.0.1 localhost
{% for hote in groups['linux'] %}
{{ hostvars[hote]['ansible_host'] }} {{ hote }}.{{ domaine_ad }} {{ hote }}
{% endfor %}
127.0.0.1 localhost
192.168.10.180 srv-journaux.ad.sorbier.example srv-journaux
192.168.10.225 srv-web.ad.sorbier.example srv-web
Ajoute un serveur au groupe linux de l'inventaire, relance : il apparaît dans le fichier, sans toucher au modèle.
Les filtres utiles
Un filtre transforme une valeur ; il s'écrit après un |, et les filtres s'enchaînent de gauche à droite. Ils servent dans les templates comme dans les playbooks. Les résultats ci-dessous ont été obtenus avec les variables de Sorbier (domaine_ad: ad.sorbier.example, serveurs_dns: [192.168.10.177, 192.168.10.181]).
| Expression | Résultat | Usage |
|---|---|---|
{{ web_port | default(80) }} | 80 si web_port n'est pas définie | Une valeur de secours |
{{ web_cle | mandatory }} | Erreur explicite si la variable manque | Une valeur obligatoire |
{{ domaine_ad | upper }} | AD.SORBIER.EXAMPLE | Le royaume Kerberos |
{{ serveurs_dns | join(', ') }} | 192.168.10.177, 192.168.10.181 | Une liste sur une ligne |
{{ serveurs_dns | first }} | 192.168.10.177 | Le premier élément |
{{ serveurs_dns | length }} | 2 | Compter |
{{ 'srv-web' | replace('srv-', '') }} | web | Remplacer un texte |
{{ 'srv-web' | regex_replace('^srv-', 'vm-') }} | vm-web | Remplacer par expression régulière |
{{ web_nom_site.split('.') | first }} | suivi | Découper une chaîne |
{{ socle_admins | map(attribute='login') | list }} | ['amercier', 'clambert'] | Extraire un champ d'une liste de dictionnaires |
{{ ['b', 'a', 'b'] | unique | sort }} | ['a', 'b'] | Dédoublonner, trier |
{{ 'yes' | bool }} | true | Convertir en booléen |
{{ '8080' | int + 1 }} | 8081 | Convertir en entier |
{{ nom | quote }} | La valeur protégée pour le shell | Dans une tâche shell |
{{ dictionnaire | to_nice_yaml }} | Le dictionnaire en YAML indenté | Générer un fichier YAML |
Les filtres viennent de Jinja2 (default, upper, join, length…), d'ansible-core (bool, regex_replace, to_nice_yaml, password_hash…) et des collections (community.general.unicode_normalize, par exemple). ansible-doc -t filter -l les liste.
Le module template
- name: Configurer chrony sur les contrôleurs de domaine
ansible.builtin.template:
src: chrony.conf.j2
dest: /etc/chrony/chrony.conf
owner: root
group: root
mode: "0644"
backup: true
notify: Redémarrer chrony
| Option | Rôle |
|---|---|
src | Le modèle, cherché dans templates/ (à côté du playbook ou dans le rôle) |
dest | Le chemin du fichier sur l'hôte |
owner, group, mode | Propriétaire, groupe et droits du fichier |
backup | Garder une copie datée de l'ancien fichier sur le serveur avant de le remplacer |
validate | Une commande qui vérifie le fichier avant de l'installer ; %s est remplacé par le chemin du fichier temporaire |
validate est la meilleure protection contre une configuration cassée. Le fichier généré est d'abord écrit dans un fichier temporaire, la commande le vérifie, et il n'est installé que si elle réussit. Le socle de Sorbier durcit SSH ainsi :
- name: Durcir le serveur SSH
ansible.builtin.template:
src: sshd-sorbier.conf.j2
dest: /etc/ssh/sshd_config.d/10-sorbier.conf
owner: root
group: root
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Recharger SSH
avec le modèle sshd-sorbier.conf.j2 :
# {{ ansible_managed }}
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LoginGraceTime 30
AllowGroups sudo {{ socle_groupe_ansible | default('ansible') }}
Une faute de frappe dans une directive fait échouer sshd -t : la tâche échoue, l'ancien fichier reste en place, SSH n'est pas rechargé, et tu ne te retrouves pas enfermé dehors. Debian 13 lit les fichiers de /etc/ssh/sshd_config.d/ grâce à la ligne Include du fichier principal : déposer un fichier à part évite de réécrire sshd_config en entier.
Les commandes de validation courantes :
| Fichier | Commande de validate |
|---|---|
sudoers.d | /usr/sbin/visudo -cf %s |
| Configuration SSH | /usr/sbin/sshd -t -f %s |
| Configuration de Nginx | Pas de validation d'un fichier isolé : tester toute la configuration avec nginx -t dans un handler |
| Configuration d'Apache | Même chose : apache2ctl configtest dans un handler, avant le rechargement |
Exemple complet : l'hôte virtuel Apache
Le rôle web de Sorbier génère l'hôte virtuel du site de suivi à partir de templates/site.conf.j2 :
# {{ ansible_managed }}
<VirtualHost *:80>
ServerName {{ web_nom_site }}
Redirect permanent / https://{{ web_nom_site }}/
</VirtualHost>
<VirtualHost *:443>
ServerName {{ web_nom_site }}
ServerAdmin {{ web_admin_mail }}
DocumentRoot {{ web_racine }}
SSLEngine on
SSLCertificateFile {{ web_certificat }}
SSLCertificateKeyFile {{ web_cle_privee }}
Header always set Strict-Transport-Security "max-age=31536000"
ErrorLog ${APACHE_LOG_DIR}/{{ web_nom_site }}-error.log
CustomLog ${APACHE_LOG_DIR}/{{ web_nom_site }}-access.log combined
</VirtualHost>
${APACHE_LOG_DIR} n'est pas une variable Jinja2 (pas d'accolades doubles) : elle reste telle quelle dans le fichier, et c'est Apache qui la remplace. Le même modèle sert pour un deuxième site : il suffit d'un autre hôte, avec d'autres valeurs de web_nom_site et web_racine dans ses host_vars.
template, copy, lineinfile ou blockinfile ?
| Module | Ce qu'il fait | Quand l'utiliser |
|---|---|---|
copy | Dépose un fichier tel quel (ou un content court) | Fichier identique partout : une page, un certificat, un script |
template | Génère tout le fichier à partir d'un modèle | Fichier qu'Ansible gère entièrement et qui varie selon l'hôte |
lineinfile | Garantit la présence (ou l'absence) d'une ligne | Un seul réglage dans un fichier géré par quelqu'un d'autre (paquet, autre outil) |
blockinfile | Insère un bloc entre deux marqueurs # BEGIN ANSIBLE MANAGED BLOCK et # END … | Quelques lignes groupées dans un fichier qu'on ne gère pas entièrement |
La règle : si Ansible est le seul à écrire le fichier, prends template, c'est le plus lisible et le plus sûr. lineinfile multiplié par dix sur le même fichier devient vite impossible à suivre : c'est le signe qu'il faut passer à un template ou à un fichier à part dans un dossier .d/.
Exercices
Exercice 1 sur 4 Le fichier resolv.conf Réussi À revoir
Écris le modèle resolv.conf.j2 qui produit, pour les serveurs de la DMZ, un fichier /etc/resolv.conf avec une ligne search pour le domaine domaine_ad, puis une ligne nameserver par serveur de serveurs_dns. Donne le fichier obtenu avec les variables de group_vars/all.yml, puis la tâche qui le déploie (propriétaire root, droits 0644).
Voir la réponseMasquer la réponse de l'exercice 1
# {{ ansible_managed }}
search {{ domaine_ad }}
{% for serveur in serveurs_dns %}
nameserver {{ serveur }}
{% endfor %}
Résultat :
# Ansible managed
search ad.sorbier.example
nameserver 192.168.10.177
nameserver 192.168.10.181
- name: Déclarer les serveurs DNS de l'annuaire
ansible.builtin.template:
src: resolv.conf.j2
dest: /etc/resolv.conf
owner: root
group: root
mode: "0644"
Vérifie avant qu'aucun autre outil ne gère ce fichier sur le serveur (systemd-resolved, NetworkManager, client DHCP), sinon il sera réécrit derrière Ansible. Sur un serveur en adresse fixe configuré par /etc/network/interfaces, comme sur une installation minimale de Debian 13, c'est bien /etc/resolv.conf qui fait foi. Pour un serveur de la DMZ, le flux DNS vers les contrôleurs doit aussi être autorisé par le pare-feu ; la matrice des flux de Sorbier lui fait plutôt utiliser le résolveur du pare-feu, 192.168.10.238 : une variable serveurs_dns dans group_vars d'un groupe dmz réglerait ce cas sans toucher au modèle. De même, le group_vars du groupe dmz redéfinit serveurs_ntp: [192.168.10.238] : le pare-feu sert l'heure à la DMZ, ce qui demande d'ajouter à la matrice des flux la règle NTP (UDP 123) de la DMZ vers 192.168.10.238 (la politique de filtrage de Sorbier dans le cours pfSense). Sans cela, le chrony de srv-web chercherait les contrôleurs de domaine, que la DMZ ne peut pas joindre, et l'heure du serveur dériverait.
Tu avais trouvé ?
Exercice 2 sur 4 Prévoir le rendu Réussi À revoir
Donne le fichier produit par ce modèle avec sites: [{nom: suivi, port: 443}, {nom: intranet, port: 8443}] et journal_detaille: false.
{% for site in sites %}
# Site {{ loop.index }} : {{ site.nom | upper }}
Listen {{ site.port }}
{% endfor %}
{% if journal_detaille %}
LogLevel debug
{% else %}
LogLevel warn
{% endif %}
Voir la réponseMasquer la réponse de l'exercice 2
# Site 1 : SUIVI
Listen 443
# Site 2 : INTRANET
Listen 8443
LogLevel warn
loop.index commence à 1. Grâce à trim_blocks, les lignes d'instructions ({% for %}, {% if %}…) ne laissent aucune ligne vide.
Tu avais trouvé ?
Exercice 3 sur 4 Le bon module Réussi À revoir
Choisis le module pour chaque besoin et justifie.
- Déposer le même certificat d'autorité interne sur tous les serveurs Linux.
- Ajouter
vm.swappiness = 10dans/etc/sysctl.conf, que le paquetprocpsfournit et met à jour. - Générer entièrement la configuration du site de suivi, avec son nom et sa racine propres à chaque serveur.
- Ajouter trois lignes d'options groupées à la fin du fichier
/etc/apt/apt.conf.d/50unattended-upgrades, qui vient du paquet.
Voir la réponseMasquer la réponse de l'exercice 3
copy: le fichier est identique partout.lineinfile, avecregexp: '^vm.swappiness'etline: vm.swappiness = 10: un seul réglage dans un fichier fourni par un paquet. Mieux encore : déposer un fichier à part,/etc/sysctl.d/60-sorbier.conf, aveccopy, et laisser le fichier du paquet intact.template: Ansible gère tout le fichier, qui varie selon l'hôte.blockinfile: un groupe de lignes, entre marqueurs, dans un fichier qu'on ne gère pas entièrement. Là aussi, un fichier à part dans/etc/apt/apt.conf.d/(par exemple52sorbier-unattended) est plus propre, parce que le paquet ne le touchera jamais.
Tu avais trouvé ?
Exercice 4 sur 4 Un login sans accent Réussi À revoir
Les comptes de Sorbier sont formés de l'initiale du prénom et du nom, en minuscules, sans accent ni espace : « Élise Bérard » donne eberard. Écris l'expression Jinja2 qui calcule ce login à partir des variables prenom et nom, avec le filtre community.general.unicode_normalize (forme NFD, qui sépare chaque lettre de son accent) et regex_replace.
Voir la réponseMasquer la réponse de l'exercice 4
{{ (prenom[0] ~ nom) | community.general.unicode_normalize(form='NFD') | regex_replace('[^A-Za-z0-9]', '') | lower }}
prenom[0] prend la première lettre, ~ concatène deux chaînes. La forme NFD décompose « É » en « E » suivi d'un accent combinant ; regex_replace supprime tout ce qui n'est pas une lettre non accentuée ou un chiffre (les accents, les espaces, les tirets) ; lower met en minuscules. « Élise Bérard » donne eberard, « Jean-Luc Le Gall » donne jlegall.
Tu avais trouvé ?