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
--checket--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.
--- # 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.
- L'indentation se fait avec des espaces, jamais des tabulations ; deux espaces par niveau est l'usage.
- Tous les éléments d'un même niveau sont alignés exactement.
- Après les deux-points d'une clé, il faut une espace.
- Une liste commence par
-; ses éléments peuvent être des valeurs simples ou des dictionnaires. - Les booléens s'écrivent
trueetfalse. YAML accepte aussiyesetno, maisansible-lintles refuse : reste surtrueetfalse. - 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.
Les principaux mots-clés d'un play :
| Mot-clé | Rôle |
|---|---|
name | Nom affiché à l'exécution ; obligatoire en pratique |
hosts | Le motif des hôtes visés |
become | true pour exécuter les tâches avec élévation de privilèges |
gather_facts | false pour ne pas collecter les facts (gain de temps quand on ne s'en sert pas) |
vars, vars_files | Variables du play, écrites dans le play ou dans des fichiers |
tasks | La liste des tâches |
handlers | Les tâches lancées seulement sur notification |
roles | Les rôles à appliquer (chapitre 8) |
Et ceux d'une tâche :
| Mot-clé | Rôle |
|---|---|
name | Ce que fait la tâche, en français, sous forme d'action ou d'état (« Installer Apache ») |
| Le module (FQCN) et ses arguments | L'action elle-même, arguments indentés sous le module |
notify | Le handler à déclencher si la tâche est changed |
become | Élever les privilèges pour cette tâche seulement |
when, loop, register, tags | Condition, 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 :
---
- 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 :
aptinstalle le paquet s'il manque.update_cache: truerafraîchit la liste des paquets avant,cache_valid_time: 3600évite de le refaire si elle date de moins d'une heure.copyenvoie le fichier du nœud de contrôle vers le serveur, avec son propriétaire et ses droits. Unsrcrelatif est cherché dans le dossierfiles/à côté du playbook.lineinfilecherche la ligne qui correspond à l'expression régulièreregexpet la remplace parline; si aucune ne correspond, il ajoutelineà la fin du fichier. Le fichiersecurity.confde Debian contientServerTokens OSetServerSignature On, plus des variantes commentées que^(début de ligne) écarte.servicedé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 :
ansible-playbook web.yml --syntax-check
ansible-playbook web.yml
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âcheslineinfileont 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 :
| Compteur | Sens |
|---|---|
ok | Tâches réussies, modifiées ou non (changed est inclus dans ok) |
changed | Tâches qui ont modifié quelque chose |
unreachable | Hôte injoignable : aucune tâche n'a pu tourner |
failed | Tâche en échec : les tâches suivantes ne sont pas exécutées pour cet hôte |
skipped | Tâches sautées par une condition |
rescued, ignored | Erreurs 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 :
- Les handlers s'exécutent à la fin du play, après toutes les tâches, pas au moment de la notification.
- Un handler notifié plusieurs fois ne s'exécute qu'une fois.
- Ils s'exécutent dans l'ordre où ils sont écrits dans la section
handlers, pas dans l'ordre des notifications. - Le nom dans
notifydoit correspondre exactement aunamedu handler, majuscules comprises. - 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-handlersexé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 :
- 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 :
ansible-playbook web.yml --check --diff --limit srv-web
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 :
commandetshellne sont pas exécutés en mode check (ils sont marquésskipped), 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
| Option | Effet |
|---|---|
--limit <motif> (-l) | Restreindre aux hôtes du motif |
--list-hosts | Afficher les hôtes visés par chaque play, sans rien exécuter |
--list-tasks | Afficher 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) |
--step | Demander confirmation avant chaque tâche |
--tags, --skip-tags | N'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 à -vvvv | Plus 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 :
---
- 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) | Cause | Correction |
|---|---|---|
mapping values are not allowed in this context | Une indentation décalée, ou un : dans une valeur non protégée | Aligner les clés ; mettre la valeur entre guillemets |
found character that cannot start any token | Une tabulation dans le fichier | Remplacer 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 nom | ansible-galaxy collection install -r requirements.yml -p collections ; vérifier le FQCN |
Unsupported parameters for (ansible.builtin.copy) module: dst | Une option mal nommée | ansible-doc du module : ici, c'est dest |
The task includes an option with an undefined variable | Variable inconnue ou mal orthographiée | ansible-inventory --host <hôte> ; vérifier le nom |
ERROR! A malformed block was discovered | Les 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 :
- installe les paquets
tcpdump,dnsutilsetrsyncen une seule tâche ; - dépose le message d'accueil
/etc/motdavec le texte « Serveur de Sorbier Logistique : accès réservé au service informatique. », droits0644, propriétaireroot; - ajoute la ligne
192.168.10.180 srv-journauxau fichier/etc/hostssi elle manque.
Voir la réponseMasquer la réponse de l'exercice 1
---
- 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.
---
- 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
become: truemanque au niveau du play : installer un paquet et écrire dans/etcdemande les droitsroot.state: installedn'est pas une valeur valide pourapt: c'estpresent(oulatest).dest: {{ chemin_chrony }}doit être entre guillemets : une valeur qui commence par{{est prise pour un dictionnaire YAML.mode: 644donne des droits faux : il fautmode: "0644".- Le handler ne correspond pas à la notification (
redémarrercontreRedémarrer), et ses argumentsnameetstatene sont pas indentés sous le module.
---
- 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
---
- 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"
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é ?