Chapitre 624 minIntermédiaire
Variables, facts, conditions et boucles
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- définir une variable au bon endroit et prévoir laquelle l'emporte ;
- utiliser les facts et le résultat d'une tâche avec
register; - n'exécuter une tâche que sous condition avec
when; - répéter une tâche avec
loop; - gérer les erreurs avec
block,rescueetfailed_when.
4 exercices corrigés1 schéma
Les variables Ansible sont des valeurs nommées (un nom de site, une liste de comptes, une adresse) que les playbooks utilisent au lieu de valeurs écrites en dur ; avec les conditions when, les boucles loop et le résultat des tâches (register), elles rendent un playbook réutilisable sur des serveurs différents. Ce chapitre explique où définir une variable et laquelle l'emporte, les facts, les conditions, les boucles, la gestion des erreurs et les tags.
Définir et utiliser une variable
Une variable se définit à de nombreux endroits ; les quatre plus courants :
---
- name: Exemple de variables
hosts: serveurs_web
become: true
vars: # 1. dans le play
paquets_web:
- apache2
- libapache2-mod-security2
vars_files: # 2. dans un fichier chargé par le play
- vars/web.yml
tasks:
- name: Installer les paquets du serveur web
ansible.builtin.apt:
name: "{{ paquets_web }}"
state: present
- dans l'inventaire,
group_varsethost_vars(chapitre 3) ; - en ligne de commande :
ansible-playbook web.yml -e "web_nom_site=test.sorbier.example".
On utilise une variable avec la syntaxe Jinja2 {{ nom }}. Pour lire un élément d'un dictionnaire ou d'une liste :
# administrateur: {login: clambert, nom: Chloé Lambert}
msg: "{{ administrateur.login }}" # notation pointée
msg: "{{ administrateur['login'] }}" # notation entre crochets, plus sûre
msg: "{{ serveurs_dns[0] }}" # premier élément d'une liste : 192.168.10.177
Préfère les crochets quand la clé contient un tiret ou coïncide avec un nom de méthode Python (items, keys, update) : administrateur.items ne lit pas la clé items.
Un nom de variable ne contient que des lettres, des chiffres et des tirets bas, et commence par une lettre. Évite les noms qui commencent par ansible_ (réservés aux variables de connexion et aux facts) et les mots réservés de Python et de YAML. Un préfixe par rôle (web_, socle_) évite les collisions : ansible-lint l'exige pour les variables des rôles.
Qui l'emporte : l'ordre de priorité
Quand une même variable est définie à plusieurs endroits, Ansible garde la plus prioritaire. L'ordre officiel compte 22 niveaux ; voici ceux que tu utiliseras, du plus faible au plus fort.
Trois conséquences à retenir :
- Les
defaultsd'un rôle perdent contre tout : on y met les valeurs par défaut, que l'inventaire remplace hôte par hôte (chapitre 8). - Les variables de l'inventaire perdent contre celles du play : une variable écrite dans
vars:d'un play s'impose à tous les hôtes, quelle que soit leur valeur danshost_vars. Pour qu'une valeur reste réglable hôte par hôte, ne la mets pas dans le play. -egagne toujours : pratique pour un test ponctuel, dangereux pour une valeur durable, qui ne serait écrite nulle part.
Ansible remplace la valeur : il ne fusionne ni les listes ni les dictionnaires d'un niveau à l'autre.
Les facts et les variables magiques
Au début de chaque play, Ansible collecte les facts de chaque hôte (chapitre 4). Dans un playbook, lis-les dans le dictionnaire ansible_facts, sans le préfixe ansible_ :
- name: Afficher le système et l'adresse de chaque serveur
ansible.builtin.debug:
msg: >-
{{ inventory_hostname }} tourne sous {{ ansible_facts['distribution'] }}
{{ ansible_facts['distribution_version'] }}, adresse {{ ansible_facts['default_ipv4']['address'] }}
ok: [srv-journaux] => {
"msg": "srv-journaux tourne sous Debian 13.7, adresse 192.168.10.180"
}
Quand un play n'utilise aucun fact, gather_facts: false lui fait gagner plusieurs secondes par hôte. Pour n'en collecter qu'une partie : gather_subset: [network].
Ansible fournit aussi des variables magiques, toujours disponibles :
| Variable | Contenu | Exemple |
|---|---|---|
inventory_hostname | Le nom de l'hôte dans l'inventaire | srv-web |
group_names | Les groupes de l'hôte en cours | ['linux', 'serveurs_web'] |
groups | Tous les groupes et leurs hôtes | groups['controleurs'] vaut ['srv-ad', 'srv-ad2'] |
hostvars | Les variables (et facts) de tous les hôtes | hostvars['srv-ad']['ansible_host'] vaut 192.168.10.177 |
ansible_check_mode | true si on est en mode --check | Sauter une tâche en simulation |
playbook_dir, role_path | Le dossier du playbook, du rôle en cours | Construire un chemin de fichier |
hostvars et groups permettent de configurer une machine à partir des autres : générer la liste des serveurs à superviser à partir du groupe linux, ou écrire dans un fichier l'adresse du contrôleur de domaine sans la recopier.
Enregistrer le résultat d'une tâche : register
register range le résultat d'une tâche dans une variable, qu'on utilise ensuite. Le module debug sert à l'afficher pendant la mise au point.
- name: Lire la version d'Apache
ansible.builtin.command: apache2 -v
register: version_apache
changed_when: false
- name: Afficher la première ligne
ansible.builtin.debug:
msg: "{{ version_apache.stdout_lines[0] }}"
Le résultat d'une commande contient notamment :
| Clé | Contenu |
|---|---|
rc | Le code de retour (0 si succès) |
stdout, stdout_lines | La sortie standard, en un texte ou en liste de lignes |
stderr | La sortie d'erreur |
changed, failed | Le statut de la tâche |
Chaque module renvoie ses propres valeurs, décrites dans la section RETURN VALUES de ansible-doc. Le module stat, par exemple, renvoie stat.exists, stat.size, stat.mode.
changed_when: false indique qu'une tâche de consultation ne modifie rien : sans lui, une command est toujours changed, et le test d'idempotence échoue pour rien.
Les conditions : when
when n'exécute une tâche que si une condition est vraie ; sinon, elle est skipped. La condition est une expression Jinja2 écrite sans {{ }}.
Exemple complet : mettre à jour les serveurs Linux un par un, et ne redémarrer que ceux qui en ont besoin. Sur Debian, le fichier /var/run/reboot-required est créé quand un nouveau noyau est installé (par le paquet unattended-upgrades, que le socle de Sorbier installe partout).
---
- name: Mettre à jour les serveurs Linux un par un
hosts: linux
become: true
serial: 1
tasks:
- name: Mettre à jour tous les paquets
ansible.builtin.apt:
upgrade: dist
update_cache: true
- name: Vérifier si un redémarrage est nécessaire
ansible.builtin.stat:
path: /var/run/reboot-required
register: fichier_redemarrage
- name: Redémarrer si une mise à jour l'exige
ansible.builtin.reboot:
reboot_timeout: 600
when: fichier_redemarrage.stat.exists
serial: 1 traite les hôtes un par un : le play entier (mise à jour, test, redémarrage) se termine sur un serveur avant de commencer sur le suivant. Si un serveur échoue, le play s'arrête avant de toucher les autres. Pour deux serveurs redondants, c'est ce qui garantit qu'il en reste toujours un en service.
Les conditions courantes :
when: ansible_facts['os_family'] == "Debian"
when: ansible_facts['distribution_major_version'] is version('13', '>=')
when: "'serveurs_web' in group_names"
when: inventory_hostname != 'srv-ad'
when: web_certificat is defined
when: resultat.rc != 0
when: # une liste : toutes les conditions (ET)
- ansible_facts['os_family'] == "Debian"
- ansible_facts['memtotal_mb'] >= 2048
when: ansible_facts['distribution'] == "Debian" or ansible_facts['distribution'] == "Ubuntu"
Une condition qui commence par une chaîne entre apostrophes doit être entièrement entre guillemets, pour que YAML ne la coupe pas : when: "'serveurs_web' in group_names".
Les boucles : loop
loop répète une tâche pour chaque élément d'une liste ; l'élément courant s'appelle item.
- name: Créer les comptes des administrateurs
ansible.builtin.user:
name: "{{ item.login }}"
comment: "{{ item.nom }}"
groups: sudo
append: true
loop: "{{ socle_admins }}"
loop_control:
label: "{{ item.login }}"
avec, dans les variables :
socle_admins:
- login: amercier
nom: Antoine Mercier
- login: clambert
nom: Chloé Lambert
loop_control.label choisit ce qui s'affiche pour chaque tour ; sans lui, Ansible affiche tout le dictionnaire, clés SSH comprises. Le résultat :
TASK [Créer les comptes des administrateurs] ***********************************
changed: [srv-journaux] => (item=amercier)
changed: [srv-journaux] => (item=clambert)
Une boucle se combine avec when, évalué à chaque tour :
loop: "{{ comptes }}"
when: item.admin
Les éléments pour lesquels la condition est fausse sont sautés un par un (skipping: [srv-journaux] => (item=stagiaire)).
Pour parcourir un dictionnaire, transforme-le en liste de paires avec le filtre dict2items : chaque item a alors une clé key et une clé value.
- name: Ouvrir les ports des services
community.general.ufw:
rule: allow
port: "{{ item.value }}"
proto: tcp
comment: "{{ item.key }}"
loop: "{{ ports_ouverts | dict2items }}"
# ports_ouverts: {ssh: "22", https: "443"}
Pour attendre qu'une condition devienne vraie (un service qui démarre, une page qui répond), utilise until avec un nombre d'essais :
- name: Attendre que le site réponde
ansible.builtin.uri:
url: https://suivi.sorbier.example
status_code: 200
register: reponse
until: reponse.status == 200
retries: 10
delay: 5
Gérer les erreurs : block, rescue, failed_when
Par défaut, une tâche en échec arrête le play pour cet hôte ; les autres hôtes continuent. Plusieurs outils permettent de faire mieux.
block, rescue, always regroupent des tâches comme un try, catch, finally :
- name: Déployer la nouvelle version du site
block:
- name: Décompresser la nouvelle version du site
ansible.builtin.unarchive:
src: files/suivi.tar.gz
dest: "{{ web_racine }}"
owner: www-data
group: www-data
- name: Vérifier que le site répond
ansible.builtin.uri:
url: "https://{{ web_nom_site }}"
validate_certs: false
rescue:
- name: Restaurer la version précédente
ansible.builtin.unarchive:
src: /var/backups/suivi-precedent.tar.gz
dest: "{{ web_racine }}"
remote_src: true
owner: www-data
group: www-data
always:
- name: Prévenir dans les journaux
ansible.builtin.command: logger "Déploiement du site terminé sur {{ inventory_hostname }}"
changed_when: false
Si une tâche du block échoue, Ansible exécute le rescue ; l'hôte n'est pas compté en échec (le récapitulatif l'indique dans rescued). Le always s'exécute dans tous les cas. Dans le rescue, la variable ansible_failed_task.name donne le nom de la tâche qui a échoué.
failed_when redéfinit l'échec, changed_when redéfinit la modification :
- name: Vérifier la configuration d'Apache
ansible.builtin.command: apache2ctl configtest
register: test_apache
changed_when: false
failed_when: "'Syntax OK' not in test_apache.stderr"
apache2ctl configtest écrit son verdict sur la sortie d'erreur, d'où stderr.
ignore_errors: true continue malgré l'échec : à réserver aux tâches vraiment facultatives, car il masque les problèmes. ansible-lint le signale quand il n'est pas justifié.
Enfin, au niveau du play :
any_errors_fatal: truearrête tous les hôtes dès qu'un seul échoue ;max_fail_percentage: 30avecserialarrête le déploiement si plus de 30 % des hôtes d'une série échouent.
Les tags
Un tag est une étiquette posée sur une tâche, un bloc, un rôle ou un play, pour n'exécuter qu'une partie d'un long playbook :
- name: Durcir le serveur SSH
ansible.builtin.template:
src: sshd-sorbier.conf.j2
dest: /etc/ssh/sshd_config.d/10-sorbier.conf
mode: "0644"
validate: /usr/sbin/sshd -t -f %s
notify: Recharger SSH
tags: [ssh, securite]
ansible-playbook site.yml --tags ssh # seulement les tâches étiquetées ssh
ansible-playbook site.yml --skip-tags securite
ansible-playbook site.yml --list-tags # les tags existants
Deux tags sont spéciaux : always (la tâche s'exécute même si on filtre sur d'autres tags, sauf --skip-tags always) et never (la tâche ne s'exécute que si on demande explicitement l'un de ses tags).
Calculer une variable pendant l'exécution : set_fact
set_fact crée une variable pour l'hôte en cours, à partir d'autres variables :
- name: Calculer l'adresse du site
ansible.builtin.set_fact:
web_url: "https://{{ web_nom_site }}/"
Elle a une priorité élevée (au-dessus des variables du play). Ne l'utilise pas pour ce qui pourrait être une simple variable d'inventaire : un set_fact ne se voit qu'à l'exécution, pas avec ansible-inventory.
Exercices
Exercice 1 sur 4 Quelle valeur l'emporte ? Réussi À revoir
Le rôle web définit web_port: 80 dans defaults/main.yml. group_vars/serveurs_web.yml contient web_port: 8080, host_vars/srv-web.yml contient web_port: 8443. Le playbook applique le rôle dans un play qui a vars: {web_port: 443}. Quelle valeur reçoit srv-web :
- avec
ansible-playbook web.yml? - avec
ansible-playbook web.yml -e web_port=9000? - si on retire la variable du play ?
- si on retire aussi le fichier
host_vars/srv-web.yml?
Voir la réponseMasquer la réponse de l'exercice 1
- 443 : les variables du play passent avant celles de l'inventaire et les
defaultsdu rôle. - 9000 :
-el'emporte toujours. Attention, c'est la chaîne"9000"; un module qui attend un entier la convertira. - 8443 :
host_varsl'emporte surgroup_varset sur lesdefaults. - 8080 : le groupe l'emporte sur les
defaults, qui ne servent que si personne d'autre ne définit la variable.
Tu avais trouvé ?
Exercice 2 sur 4 Écrire des conditions Réussi À revoir
Écris la condition when de chaque tâche.
- Installer
ufwseulement sur les systèmes de la famille Debian. - Configurer l'hôte virtuel seulement sur les hôtes du groupe
serveurs_web. - Redémarrer seulement si la tâche enregistrée dans
maja modifié quelque chose. - Installer un paquet seulement si la variable booléenne
installer_outilsvaut vrai, même si elle est passée en-e installer_outils=false. - Afficher un avertissement si la partition
/a moins de 2 Go libres, sachant queansible_facts['mounts']est une liste de dictionnaires avec les clésmountetsize_available(en octets).
Voir la réponseMasquer la réponse de l'exercice 2
when: ansible_facts['os_family'] == "Debian" # 1
when: "'serveurs_web' in group_names" # 2
when: maj is changed # 3
when: installer_outils | bool # 4
when: >- # 5
(ansible_facts['mounts'] | selectattr('mount', 'equalto', '/')
| map(attribute='size_available') | first) < 2 * 1024 ** 3
Pour la 3, maj.changed fonctionne aussi ; le test is changed est l'écriture recommandée. Pour la 4, sans | bool, -e installer_outils=false donne la chaîne "false", refusée comme condition depuis ansible-core 2.19. Pour la 5, selectattr garde le point de montage /, map extrait sa place libre, first prend la valeur ; 2 Go font 2 × 1024³ = 2 147 483 648 octets.
Tu avais trouvé ?
Exercice 3 sur 4 Boucle et comptes Réussi À revoir
Le fichier group_vars/linux.yml contient la liste ci-dessous. Écris la tâche qui crée chaque compte avec son nom complet et son shell /bin/bash, n'ajoute au groupe sudo que les administrateurs, et n'affiche que le login à chaque tour.
comptes_linux:
- login: amercier
nom: Antoine Mercier
admin: true
- login: mdiallo
nom: Moussa Diallo
admin: true
- login: lgirard
nom: Lucas Girard
admin: false
Voir la réponseMasquer la réponse de l'exercice 3
- name: Créer les comptes Linux
ansible.builtin.user:
name: "{{ item.login }}"
comment: "{{ item.nom }}"
shell: /bin/bash
groups: "{{ ['sudo'] if item.admin else [] }}"
append: true
loop: "{{ comptes_linux }}"
loop_control:
label: "{{ item.login }}"
L'expression ['sudo'] if item.admin else [] est un « si » en ligne de Jinja2 : elle donne une liste avec sudo pour les administrateurs, une liste vide pour les autres. Avec append: true, une liste vide ne retire aucun groupe. Une autre solution, plus lisible pour un débutant, fait deux tâches : une qui crée tous les comptes, une qui ajoute au groupe sudo avec when: item.admin.
Tu avais trouvé ?
Exercice 4 sur 4 Rattraper une erreur Réussi À revoir
Le serveur srv-journaux doit télécharger un fichier de configuration depuis un serveur interne. Si le téléchargement échoue, il faut déposer une configuration de secours à partir de files/secours.conf, et dans tous les cas écrire une ligne dans le journal système. Écris le bloc.
Voir la réponseMasquer la réponse de l'exercice 4
- name: Récupérer la configuration de référence
block:
- name: Télécharger la configuration
ansible.builtin.get_url:
url: https://srv-gestion.ad.sorbier.example/config/journaux.conf
dest: /etc/sorbier/journaux.conf
mode: "0644"
rescue:
- name: Déposer la configuration de secours
ansible.builtin.copy:
src: files/secours.conf
dest: /etc/sorbier/journaux.conf
mode: "0644"
always:
- name: Tracer l'opération dans le journal système
ansible.builtin.command: logger "Configuration des journaux mise à jour par Ansible"
changed_when: false
Si le téléchargement échoue (serveur injoignable, certificat refusé, 404), Ansible exécute le rescue et l'hôte compte en rescued, pas en failed. Le dossier /etc/sorbier doit exister : une tâche ansible.builtin.file avec state: directory avant le bloc le garantit.
Tu avais trouvé ?