Aller au contenu
Réussir mon BTS SIO

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

Par exemple

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, rescue et failed_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 :

yaml
---
- 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
  1. dans l'inventaire, group_vars et host_vars (chapitre 3) ;
  2. 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 :

yaml
# 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.

plus forteplus faible11. -e en ligne de commande : gagne toujours10. set_fact et register, pendant l'exécution9. variables d'un bloc ou d'une tâche8. vars d'un rôle (roles/x/vars/main.yml)7. vars_files du play6. vars du play5. facts collectés sur l'hôte4. host_vars/<hôte>3. group_vars/<groupe>2. group_vars/all1. defaults d'un rôle : remplacés par tout le restel'inventaire : sous les variables écrites dans les playbooks
L'ordre de priorité des variables, simplifié : la plus haute l'emporte

Trois conséquences à retenir :

  1. Les defaults d'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).
  2. 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 dans host_vars. Pour qu'une valeur reste réglable hôte par hôte, ne la mets pas dans le play.
  3. -e gagne 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_ :

yaml
    - 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'] }}
text
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 :

VariableContenuExemple
inventory_hostnameLe nom de l'hôte dans l'inventairesrv-web
group_namesLes groupes de l'hôte en cours['linux', 'serveurs_web']
groupsTous les groupes et leurs hôtesgroups['controleurs'] vaut ['srv-ad', 'srv-ad2']
hostvarsLes variables (et facts) de tous les hôteshostvars['srv-ad']['ansible_host'] vaut 192.168.10.177
ansible_check_modetrue si on est en mode --checkSauter une tâche en simulation
playbook_dir, role_pathLe dossier du playbook, du rôle en coursConstruire 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.

yaml
    - 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
rcLe code de retour (0 si succès)
stdout, stdout_linesLa sortie standard, en un texte ou en liste de lignes
stderrLa sortie d'erreur
changed, failedLe 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).

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

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

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

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

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

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

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

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

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

yaml
    - 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: true arrête tous les hôtes dès qu'un seul échoue ;
  • max_fail_percentage: 30 avec serial arrê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 :

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

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

  1. avec ansible-playbook web.yml ?
  2. avec ansible-playbook web.yml -e web_port=9000 ?
  3. si on retire la variable du play ?
  4. si on retire aussi le fichier host_vars/srv-web.yml ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. 443 : les variables du play passent avant celles de l'inventaire et les defaults du rôle.
  2. 9000 : -e l'emporte toujours. Attention, c'est la chaîne "9000" ; un module qui attend un entier la convertira.
  3. 8443 : host_vars l'emporte sur group_vars et sur les defaults.
  4. 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.

  1. Installer ufw seulement sur les systèmes de la famille Debian.
  2. Configurer l'hôte virtuel seulement sur les hôtes du groupe serveurs_web.
  3. Redémarrer seulement si la tâche enregistrée dans maj a modifié quelque chose.
  4. Installer un paquet seulement si la variable booléenne installer_outils vaut vrai, même si elle est passée en -e installer_outils=false.
  5. Afficher un avertissement si la partition / a moins de 2 Go libres, sachant que ansible_facts['mounts'] est une liste de dictionnaires avec les clés mount et size_available (en octets).
Voir la réponseMasquer la réponse de l'exercice 2
yaml
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.

yaml
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
yaml
    - 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
yaml
    - 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é ?

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 : Templates Jinja2 et fichiers de configurationRevoir le sommaire du cours