Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 template et le valider avant de l'installer ;
  • choisir entre template, copy, lineinfile et blockinfile.

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.

Nœud de contrôlesrv-journauxchrony.conf.j2{% for s in serveurs_ntp %}server {{ s }} iburst{% endfor %}driftfile …Variables de srv-journauxserveurs_ntp: - 192.168.10.177 - 192.168.10.181Jinja2rendu/etc/chrony/chrony.conf# Ansible managedserver 192.168.10.177 iburstserver 192.168.10.181 iburstdriftfile …Le rendu a lieu sur le nœud de contrôle : seul le fichier final est envoyé au serveur.
Un modèle et des variables donnent, sur le nœud de contrôle, le fichier propre à chaque hôte

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élimiteurRôleExemple
{{ … }}Afficher une valeurServerName {{ 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 :

text
# {{ 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] :

text
# 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 :

text
{% 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 :

text
127.0.0.1 localhost
{% for hote in groups['linux'] %}
{{ hostvars[hote]['ansible_host'] }} {{ hote }}.{{ domaine_ad }} {{ hote }}
{% endfor %}
text
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]).

ExpressionRésultatUsage
{{ web_port | default(80) }}80 si web_port n'est pas définieUne valeur de secours
{{ web_cle | mandatory }}Erreur explicite si la variable manqueUne valeur obligatoire
{{ domaine_ad | upper }}AD.SORBIER.EXAMPLELe royaume Kerberos
{{ serveurs_dns | join(', ') }}192.168.10.177, 192.168.10.181Une liste sur une ligne
{{ serveurs_dns | first }}192.168.10.177Le premier élément
{{ serveurs_dns | length }}2Compter
{{ 'srv-web' | replace('srv-', '') }}webRemplacer un texte
{{ 'srv-web' | regex_replace('^srv-', 'vm-') }}vm-webRemplacer par expression régulière
{{ web_nom_site.split('.') | first }}suiviDé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 }}trueConvertir en booléen
{{ '8080' | int + 1 }}8081Convertir en entier
{{ nom | quote }}La valeur protégée pour le shellDans 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

yaml
    - 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
OptionRôle
srcLe modèle, cherché dans templates/ (à côté du playbook ou dans le rôle)
destLe chemin du fichier sur l'hôte
owner, group, modePropriétaire, groupe et droits du fichier
backupGarder une copie datée de l'ancien fichier sur le serveur avant de le remplacer
validateUne 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 :

yaml
    - 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 :

text
# {{ 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 :

FichierCommande de validate
sudoers.d/usr/sbin/visudo -cf %s
Configuration SSH/usr/sbin/sshd -t -f %s
Configuration de NginxPas de validation d'un fichier isolé : tester toute la configuration avec nginx -t dans un handler
Configuration d'ApacheMê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 :

text
# {{ 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 ?

ModuleCe qu'il faitQuand l'utiliser
copyDépose un fichier tel quel (ou un content court)Fichier identique partout : une page, un certificat, un script
templateGénère tout le fichier à partir d'un modèleFichier qu'Ansible gère entièrement et qui varie selon l'hôte
lineinfileGarantit la présence (ou l'absence) d'une ligneUn seul réglage dans un fichier géré par quelqu'un d'autre (paquet, autre outil)
blockinfileInsè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
text
# {{ ansible_managed }}
search {{ domaine_ad }}
{% for serveur in serveurs_dns %}
nameserver {{ serveur }}
{% endfor %}

Résultat :

text
# Ansible managed
search ad.sorbier.example
nameserver 192.168.10.177
nameserver 192.168.10.181
yaml
    - 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.

text
{% 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
text
# 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.

  1. Déposer le même certificat d'autorité interne sur tous les serveurs Linux.
  2. Ajouter vm.swappiness = 10 dans /etc/sysctl.conf, que le paquet procps fournit et met à jour.
  3. Générer entièrement la configuration du site de suivi, avec son nom et sa racine propres à chaque serveur.
  4. 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
  1. copy : le fichier est identique partout.
  2. lineinfile, avec regexp: '^vm.swappiness' et line: 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, avec copy, et laisser le fichier du paquet intact.
  3. template : Ansible gère tout le fichier, qui varie selon l'hôte.
  4. 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 exemple 52sorbier-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
text
{{ (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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Rôles Ansible : créer et réutiliser un rôleRevoir le sommaire du cours