Aller au contenu
Réussir mon BTS SIO

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

Par exemple

Chapitre 525 minIntermédiaire

Écrire un playbook Ansible

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire un playbook valide : plays, tâches et modules ;
  • lire le résultat d'une exécution et son récapitulatif ;
  • redémarrer un service seulement quand il le faut, avec un handler ;
  • tester avant d'appliquer avec --check et --diff ;
  • limiter une exécution à certains hôtes ou à certaines tâches.

4 exercices corrigés1 schéma

Un playbook Ansible est un fichier YAML qui décrit, dans l'ordre, les tâches à appliquer à des groupes d'hôtes : c'est la « recette » d'une configuration, qu'on versionne et qu'on rejoue. Ce chapitre rappelle la syntaxe YAML, détaille la structure d'un playbook, en écrit un qui installe le serveur web de Sorbier, explique comment lire son résultat, ce que sont les handlers, et comment tester un playbook avant de l'appliquer.

YAML en cinq minutes

YAML est un format de données lisible : pas de balises ni d'accolades, la structure vient de l'indentation. Six règles suffisent pour écrire des playbooks.

yaml
---                            # début de document (facultatif mais d'usage)
# Un commentaire commence par un dièse
nom_du_site: suivi.sorbier.example   # une clé, deux-points, une espace, une valeur
port: 443                            # un nombre
https_actif: true                    # un booléen : true ou false

serveurs_dns:                  # une liste : un tiret et une espace par élément
  - 192.168.10.177
  - 192.168.10.181

administrateur:                # un dictionnaire : des clés indentées sous la clé parente
  login: clambert
  nom: Chloé Lambert

banniere: |                    # un texte sur plusieurs lignes, retours à la ligne conservés
  Accès réservé au personnel autorisé.
  Toute connexion est journalisée.
  1. L'indentation se fait avec des espaces, jamais des tabulations ; deux espaces par niveau est l'usage.
  2. Tous les éléments d'un même niveau sont alignés exactement.
  3. Après les deux-points d'une clé, il faut une espace.
  4. Une liste commence par -  ; ses éléments peuvent être des valeurs simples ou des dictionnaires.
  5. Les booléens s'écrivent true et false. YAML accepte aussi yes et no, mais ansible-lint les refuse : reste sur true et false.
  6. Mets entre guillemets une valeur qui commence par {{, qui contient : ou #, ou qui doit rester une chaîne.

La structure d'un playbook

Un playbook est une liste de plays. Chaque play associe un groupe d'hôtes à une liste de tâches. Chaque tâche appelle un module.

web.yml : un playbook, ici avec un seul playPlay : Installer et configurer le serveur webhosts: serveurs_webbecome: truetasks:1. Installer Apacheansible.builtin.apt2. Déposer la pageansible.builtin.copy3. Masquer la versionansible.builtin.lineinfile4. Démarrer Apacheansible.builtin.servicehandlers:Recharger Apacheansible.builtin.servicenotify, si latâche 3 estchanged ;en fin de playde hauten bas
Un playbook : des plays, qui appliquent des tâches à des hôtes ; chaque tâche appelle un module

Les principaux mots-clés d'un play :

Mot-cléRôle
nameNom affiché à l'exécution ; obligatoire en pratique
hostsLe motif des hôtes visés
becometrue pour exécuter les tâches avec élévation de privilèges
gather_factsfalse pour ne pas collecter les facts (gain de temps quand on ne s'en sert pas)
vars, vars_filesVariables du play, écrites dans le play ou dans des fichiers
tasksLa liste des tâches
handlersLes tâches lancées seulement sur notification
rolesLes rôles à appliquer (chapitre 8)

Et ceux d'une tâche :

Mot-cléRôle
nameCe que fait la tâche, en français, sous forme d'action ou d'état (« Installer Apache »)
Le module (FQCN) et ses argumentsL'action elle-même, arguments indentés sous le module
notifyLe handler à déclencher si la tâche est changed
becomeÉlever les privilèges pour cette tâche seulement
when, loop, register, tagsCondition, boucle, enregistrement du résultat, étiquettes (chapitre 6)

Un premier playbook : le serveur web de Sorbier

Le besoin : sur srv-web, Apache doit être installé, servir une page d'accueil, ne pas afficher sa version dans les en-têtes et les pages d'erreur, et démarrer automatiquement. Le projet contient un fichier files/index.html et le playbook web.yml :

yaml
---
- name: Installer et configurer le serveur web
  hosts: serveurs_web
  become: true

  tasks:
    - name: Installer Apache
      ansible.builtin.apt:
        name: apache2
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Déposer la page d'accueil
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
        owner: www-data
        group: www-data
        mode: "0644"

    - name: Masquer la version d'Apache dans les en-têtes
      ansible.builtin.lineinfile:
        path: /etc/apache2/conf-available/security.conf
        regexp: "^ServerTokens"
        line: ServerTokens Prod
      notify: Recharger Apache

    - name: Masquer la signature d'Apache dans les pages d'erreur
      ansible.builtin.lineinfile:
        path: /etc/apache2/conf-available/security.conf
        regexp: "^ServerSignature"
        line: ServerSignature Off
      notify: Recharger Apache

    - name: Démarrer Apache et l'activer au démarrage
      ansible.builtin.service:
        name: apache2
        state: started
        enabled: true

  handlers:
    - name: Recharger Apache
      ansible.builtin.service:
        name: apache2
        state: reloaded

Ce que fait chaque tâche :

  • apt installe le paquet s'il manque. update_cache: true rafraîchit la liste des paquets avant, cache_valid_time: 3600 évite de le refaire si elle date de moins d'une heure.
  • copy envoie le fichier du nœud de contrôle vers le serveur, avec son propriétaire et ses droits. Un src relatif est cherché dans le dossier files/ à côté du playbook.
  • lineinfile cherche la ligne qui correspond à l'expression régulière regexp et la remplace par line ; si aucune ne correspond, il ajoute line à la fin du fichier. Le fichier security.conf de Debian contient ServerTokens OS et ServerSignature On, plus des variantes commentées que ^ (début de ligne) écarte.
  • service démarre Apache s'il est arrêté et l'active au démarrage du système.

Lancer le playbook et lire le résultat

Commence par vérifier la syntaxe, puis lance :

bash
ansible-playbook web.yml --syntax-check
ansible-playbook web.yml
text
PLAY [Installer et configurer le serveur web] **********************************

TASK [Gathering Facts] *********************************************************
ok: [srv-web]

TASK [Installer Apache] ********************************************************
changed: [srv-web]

TASK [Déposer la page d'accueil] ***********************************************
changed: [srv-web]

TASK [Masquer la version d'Apache dans les en-têtes] ***************************
changed: [srv-web]

TASK [Masquer la signature d'Apache dans les pages d'erreur] *******************
changed: [srv-web]

TASK [Démarrer Apache et l'activer au démarrage] *******************************
ok: [srv-web]

RUNNING HANDLER [Recharger Apache] *********************************************
changed: [srv-web]

PLAY RECAP *********************************************************************
srv-web                    : ok=7    changed=5    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Lis-le de haut en bas :

  • Gathering Facts : la collecte des facts, ajoutée automatiquement au début du play.
  • Installer Apache, changed : le paquet n'était pas là.
  • Démarrer Apache, ok : sur Debian, l'installation du paquet démarre et active déjà le service ; la tâche le constate et ne fait rien. Elle reste utile : elle garantit l'état même si quelqu'un arrête le service.
  • RUNNING HANDLER : les deux tâches lineinfile ont notifié « Recharger Apache » ; le handler s'exécute une seule fois, à la fin du play.

Le récapitulatif (PLAY RECAP) compte, pour chaque hôte :

CompteurSens
okTâches réussies, modifiées ou non (changed est inclus dans ok)
changedTâches qui ont modifié quelque chose
unreachableHôte injoignable : aucune tâche n'a pu tourner
failedTâche en échec : les tâches suivantes ne sont pas exécutées pour cet hôte
skippedTâches sautées par une condition
rescued, ignoredErreurs rattrapées par un rescue ou ignorées par ignore_errors (chapitre 6)

Relance le même playbook : toutes les tâches sont ok, le handler ne s'exécute pas (personne ne l'a notifié), et le récapitulatif affiche ok=6 changed=0. Le playbook est idempotent.

Les handlers

Un handler est une tâche qui ne s'exécute que si une autre tâche l'a notifiée (notify) et que cette tâche a été changed. On s'en sert pour redémarrer ou recharger un service après une modification de sa configuration, et seulement dans ce cas.

Les règles à connaître :

  1. Les handlers s'exécutent à la fin du play, après toutes les tâches, pas au moment de la notification.
  2. Un handler notifié plusieurs fois ne s'exécute qu'une fois.
  3. Ils s'exécutent dans l'ordre où ils sont écrits dans la section handlers, pas dans l'ordre des notifications.
  4. Le nom dans notify doit correspondre exactement au name du handler, majuscules comprises.
  5. Si une tâche échoue après une notification, l'hôte s'arrête et le handler ne s'exécute pas : la configuration est modifiée mais le service n'est pas rechargé. L'option --force-handlers exécute quand même les handlers notifiés.

Pour exécuter les handlers en attente au milieu d'un play (par exemple recharger Apache avant de tester le site), insère la tâche spéciale :

yaml
    - name: Appliquer les rechargements en attente
      ansible.builtin.meta: flush_handlers

Plusieurs handlers peuvent aussi écouter un même sujet avec listen : une seule notification les déclenche tous, dans leur ordre d'écriture. Le rôle web du chapitre 8 s'en sert pour tester la configuration d'Apache juste avant de la recharger.

Tester avant d'appliquer : --check et --diff

Deux options rendent un playbook prudent :

  • --check (-C) simule l'exécution : chaque module dit s'il ferait une modification, sans la faire.
  • --diff (-D) affiche les différences ligne par ligne pour les fichiers modifiés.

Combinées, elles montrent exactement ce qu'un playbook changerait sur un serveur déjà en service. Après qu'un collègue a remis ServerTokens OS à la main sur srv-web :

bash
ansible-playbook web.yml --check --diff --limit srv-web
text
TASK [Masquer la version d'Apache dans les en-têtes] ***************************
--- before: /etc/apache2/conf-available/security.conf (content)
+++ after: /etc/apache2/conf-available/security.conf (content)
@@ -9,7 +9,7 @@
 # Set to one of:  Full | OS | Minimal | Minor | Major | Prod
 # where Full conveys the most information, and Prod the least.
 #ServerTokens Minimal
-ServerTokens OS
+ServerTokens Prod
 #ServerTokens Full
 
 #

changed: [srv-web]

C'est aussi un outil de détection de la dérive : un --check qui annonce des changed sur un serveur déjà configuré signifie que quelqu'un l'a modifié à la main.

Les limites du mode check :

  • command et shell ne sont pas exécutés en mode check (ils sont marqués skipped), puisque Ansible ne peut pas savoir ce qu'ils feraient ;
  • une tâche qui dépend d'une précédente peut échouer : sur un serveur neuf, la tâche service échoue en mode check parce qu'Apache n'a pas vraiment été installé ;
  • pour forcer une tâche de consultation à s'exécuter même en mode check, ajoute-lui check_mode: false.

Choisir ce qu'on exécute

OptionEffet
--limit <motif> (-l)Restreindre aux hôtes du motif
--list-hostsAfficher les hôtes visés par chaque play, sans rien exécuter
--list-tasksAfficher la liste des tâches, sans rien exécuter
--start-at-task "<nom>"Commencer à une tâche précise (après la correction d'une erreur, par exemple)
--stepDemander confirmation avant chaque tâche
--tags, --skip-tagsN'exécuter que les tâches étiquetées, ou toutes sauf elles (chapitre 6)
-e "<variable>=<valeur>"Passer une variable en ligne de commande (priorité maximale)
-v à -vvvvPlus de détails ; -v affiche le résultat de chaque tâche

Plusieurs plays et le playbook principal

Un fichier peut contenir plusieurs plays, exécutés l'un après l'autre : un play pour les serveurs Linux, un autre pour les serveurs web. Et un playbook peut en importer d'autres avec import_playbook. L'usage est d'avoir un playbook principal, souvent nommé site.yml, qui assemble toute l'infrastructure :

yaml
---
- name: Socle des serveurs Linux
  ansible.builtin.import_playbook: socle.yml

- name: Serveurs web
  ansible.builtin.import_playbook: web.yml

- name: Serveurs Windows
  ansible.builtin.import_playbook: windows.yml

ansible-playbook site.yml reconstruit tout ; ansible-playbook web.yml ne traite que les serveurs web.

Les erreurs fréquentes

Message (extrait)CauseCorrection
mapping values are not allowed in this contextUne indentation décalée, ou un : dans une valeur non protégéeAligner les clés ; mettre la valeur entre guillemets
found character that cannot start any tokenUne tabulation dans le fichierRemplacer les tabulations par des espaces (règle l'éditeur)
couldn't resolve module/action 'community.general.ufw'La collection n'est pas installée, ou faute de frappe dans le nomansible-galaxy collection install -r requirements.yml -p collections ; vérifier le FQCN
Unsupported parameters for (ansible.builtin.copy) module: dstUne option mal nomméeansible-doc du module : ici, c'est dest
The task includes an option with an undefined variableVariable inconnue ou mal orthographiéeansible-inventory --host <hôte> ; vérifier le nom
ERROR! A malformed block was discoveredLes tâches ne sont pas une liste (tiret oublié)Chaque tâche commence par - name:

Exercices

Exercice 1 sur 4 Écrire un playbook Réussi À revoir

Écris le playbook outils.yml qui, sur tous les serveurs Linux :

  1. installe les paquets tcpdump, dnsutils et rsync en une seule tâche ;
  2. dépose le message d'accueil /etc/motd avec le texte « Serveur de Sorbier Logistique : accès réservé au service informatique. », droits 0644, propriétaire root ;
  3. ajoute la ligne 192.168.10.180 srv-journaux au fichier /etc/hosts si elle manque.
Voir la réponseMasquer la réponse de l'exercice 1
yaml
---
- name: Installer les outils et le message d'accueil
  hosts: linux
  become: true

  tasks:
    - name: Installer les outils de diagnostic
      ansible.builtin.apt:
        name:
          - tcpdump
          - dnsutils
          - rsync
        state: present
        update_cache: true
        cache_valid_time: 3600

    - name: Déposer le message d'accueil
      ansible.builtin.copy:
        content: "Serveur de Sorbier Logistique : accès réservé au service informatique.\n"
        dest: /etc/motd
        owner: root
        group: root
        mode: "0644"

    - name: Déclarer srv-journaux dans le fichier hosts
      ansible.builtin.lineinfile:
        path: /etc/hosts
        line: 192.168.10.180 srv-journaux

apt accepte une liste dans name : une seule tâche, une seule transaction, plus rapide qu'une boucle. Le texte du message contient : , d'où les guillemets. En pratique, le DNS de l'Active Directory résout déjà srv-journaux : une entrée dans /etc/hosts ne se justifie que pour une machine qui ne l'interroge pas, comme un serveur de la DMZ.

Tu avais trouvé ?

Exercice 2 sur 4 Trouver les erreurs Réussi À revoir

Ce playbook contient cinq erreurs. Trouve-les et corrige-les.

yaml
---
- name: Configurer chrony
  hosts: linux
  tasks:
    - name: Installer chrony
      ansible.builtin.apt:
        name: chrony
        state: installed

    - name: Déposer la configuration
      ansible.builtin.copy:
        src: files/chrony.conf
        dest: {{ chemin_chrony }}
        mode: 644
      notify: redémarrer chrony

  handlers:
    - name: Redémarrer chrony
      ansible.builtin.service:
      name: chrony
      state: restarted
Voir la réponseMasquer la réponse de l'exercice 2
  1. become: true manque au niveau du play : installer un paquet et écrire dans /etc demande les droits root.
  2. state: installed n'est pas une valeur valide pour apt : c'est present (ou latest).
  3. dest: {{ chemin_chrony }} doit être entre guillemets : une valeur qui commence par {{ est prise pour un dictionnaire YAML.
  4. mode: 644 donne des droits faux : il faut mode: "0644".
  5. Le handler ne correspond pas à la notification (redémarrer contre Redémarrer), et ses arguments name et state ne sont pas indentés sous le module.
yaml
---
- name: Configurer chrony
  hosts: linux
  become: true
  tasks:
    - name: Installer chrony
      ansible.builtin.apt:
        name: chrony
        state: present

    - name: Déposer la configuration
      ansible.builtin.copy:
        src: files/chrony.conf
        dest: "{{ chemin_chrony }}"
        mode: "0644"
      notify: Redémarrer chrony

  handlers:
    - name: Redémarrer chrony
      ansible.builtin.service:
        name: chrony
        state: restarted

Tu avais trouvé ?

Exercice 3 sur 4 Prévoir le récapitulatif Réussi À revoir

Sur un serveur où Apache est déjà installé et démarré, où index.html a déjà le bon contenu, mais où quelqu'un a remis ServerSignature On, tu lances web.yml. Donne la ligne PLAY RECAP attendue, et dis si le handler s'exécute.

Voir la réponseMasquer la réponse de l'exercice 3

Six tâches, Gathering Facts compris : seule la tâche ServerSignature est changed. Elle notifie le handler, qui s'exécute et compte comme une tâche de plus, changed lui aussi (un rechargement est toujours une modification). Récapitulatif : srv-web : ok=7 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0.

Tu avais trouvé ?

Exercice 4 sur 4 Un playbook pour préparer les serveurs Réussi À revoir

Le chapitre 2 préparait le compte ansible à la main. Écris le playbook preparer.yml qui fait la même chose (compte ansible, fichier sudoers.d validé, clé publique de ~/.ssh/ansible_ed25519.pub limitée au bastion), sur un hôte passé en variable cible. Donne la commande pour l'appliquer à srv-web avec le compte clambert, qui a un mot de passe sudo. Attention : group_vars/linux.yml fixe ansible_user: ansible.

Voir la réponseMasquer la réponse de l'exercice 4
yaml
---
- name: Préparer un serveur pour Ansible
  hosts: "{{ cible }}"
  become: true

  tasks:
    - name: Créer le compte ansible, sans mot de passe utilisable
      ansible.builtin.user:
        name: ansible
        comment: Compte Ansible
        shell: /bin/bash

    - name: Autoriser sudo sans mot de passe
      ansible.builtin.copy:
        content: "ansible ALL=(ALL:ALL) NOPASSWD: ALL\n"
        dest: /etc/sudoers.d/ansible
        owner: root
        group: root
        mode: "0440"
        validate: /usr/sbin/visudo -cf %s

    - name: Autoriser la clé de l'administrateur, depuis le bastion seulement
      ansible.posix.authorized_key:
        user: ansible
        key: "{{ lookup('ansible.builtin.file', '~/.ssh/ansible_ed25519.pub') }}"
        key_options: from="192.168.10.210"
bash
ansible-playbook preparer.yml -e cible=srv-web -e ansible_user=clambert -K

L'option -u clambert ne suffirait pas : une variable d'inventaire (ansible_user dans group_vars/linux.yml) l'emporte sur -u. Une variable passée par -e a la priorité maximale. -K demande le mot de passe sudo de Chloé. validate fait vérifier le fichier par visudo avant de l'installer : un fichier invalide n'est jamais mis en place. Un compte créé par user sans option password a un mot de passe verrouillé : il n'accepte que les clés. Le lookup file lit la clé sur le nœud de contrôle, pas sur le serveur.

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 : Variables, facts, conditions et bouclesRevoir le sommaire du cours