Ansible : inventaire, playbooks, rôles ; gestion de configuration contre provisionnement ; secrets et coffres-forts.
Le chapitre 7 s'arrête sur une machine vide. Elle existe, elle a une adresse, un disque et un groupe de sécurité — et rien ne tourne dessus.
Reste à installer les paquets, déposer les fichiers de configuration, créer les comptes de service, démarrer les processus. C'est la gestion de configuration, et elle répond à une question que le provisionnement ne pose pas : non pas « quelles machines existent ? » mais « qu'y a-t-il dessus ? ».
Deux outils, une frontière mobile
| Provisionnement (Terraform) | Configuration (Ansible) | |
|---|---|---|
| Objet | machines, réseaux, bases, DNS | paquets, fichiers, services, comptes |
| Question | qu'est-ce qui existe ? | qu'y a-t-il dessus ? |
| Modèle | état désiré, comparé à un état enregistré | état désiré, vérifié à chaque exécution |
| Fréquence | à chaque changement d'infrastructure | à chaque changement de configuration |
La frontière est moins nette qu'elle n'en a l'air, et elle s'est déplacée ces dix dernières années.
Dans le modèle historique, on créait des serveurs durables et on les configurait à répétition, pour rattraper la dérive du chapitre 7. Dans le modèle immuable, la configuration est cuite dans l'image au moment de la construction, et la machine en service n'est plus jamais touchée. Le rôle d'Ansible se déplace alors : il sert à construire l'image plutôt qu'à entretenir des serveurs.
Il reste largement employé, pour trois usages qui ne disparaissent pas. Le matériel physique et les environnements sur site, qu'on ne remplace pas d'un clic. Les tâches ponctuelles d'exploitation — appliquer un correctif de sécurité sur trente machines, faire tourner une migration. Et la configuration d'appareils qui ne sont pas des serveurs : équipements réseau, systèmes embarqués.
Ansible en quatre notions
Ansible se distingue par une absence : il n'y a rien à installer sur les machines cibles. Il se connecte en SSH, y dépose un module, l'exécute, récupère le résultat, et repart. Pas d'agent, pas de service à maintenir, pas de port supplémentaire à ouvrir — c'est la raison principale de son adoption.
L'inventaire liste les machines et les regroupe.
web: hosts: web1.example.net: web2.example.net:base: hosts: db1.example.net:Le playbook associe des groupes à des tâches.
- hosts: web become: true tasks: - name: installer nginx ansible.builtin.package: name: nginx state: present - name: déposer la configuration ansible.builtin.template: src: nginx.conf.j2 dest: /etc/nginx/nginx.conf mode: "0644" notify: recharger nginx - name: activer le service ansible.builtin.service: name: nginx state: started enabled: true handlers: - name: recharger nginx ansible.builtin.service: name: nginx state: reloadedLes tâches déclarent un état, pas une commande : state: present signifie « il doit être
installé », et non « installe-le ». La différence est celle du chapitre 7, et elle porte tout ce
qui suit.
Les rôles regroupent tâches, fichiers, gabarits et variables en une unité réutilisable —
l'équivalent du module Terraform. Un rôle nginx s'applique à plusieurs projets ; ses variables
en font l'ajustement.
À noter enfin les gestionnaires (handlers) : une tâche notifie un gestionnaire, qui ne s'exécute qu'une fois, à la fin, et seulement si quelque chose a réellement changé. C'est ce qui évite de recharger nginx trois fois — ou de le recharger alors que rien n'a bougé.
L'idempotence, en pratique
C'est le point du chapitre, et il se mesure.
Une exécution Ansible se termine par un décompte : ok pour les tâches qui n'ont rien eu à
faire, changed pour celles qui ont modifié quelque chose.
PLAY RECAPweb1 : ok=12 changed=3 unreachable=0 failed=0La deuxième exécution du même playbook doit afficher changed=0. C'est le test
d'idempotence, et il devrait faire partie du pipeline : un playbook qui rapporte des
modifications alors que rien n'a bougé ne permet plus de distinguer « la configuration a
dérivé » de « ce playbook ment ».
Or il existe une porte de sortie qui casse cette propriété : les modules command et shell,
qui exécutent une commande arbitraire. Ansible ne peut pas savoir si elle a changé quelque
chose, alors il déclare changed à chaque fois.
- name: télécharger l'outil # NON idempotent ansible.builtin.shell: curl -o /opt/outil https://example.net/outilTrois façons de rétablir la propriété, par ordre de préférence. Employer un module dédié —
ici get_url, qui ne retélécharge pas si le fichier est déjà là. À défaut, déclarer une
condition de création : creates: /opt/outil fait sauter la tâche si le fichier existe. En
dernier recours, dire soi-même quand il y a eu changement, avec changed_when sur la sortie
de la commande.
La règle d'usage : shell est l'aveu qu'aucun module ne convient. C'est parfois vrai, et cela
doit rester rare — chaque occurrence est une tâche dont on ne sait plus si elle est sûre à
relancer.
Les secrets
Un playbook a besoin de mots de passe de base, de clés d'interface, de certificats. Ils ne peuvent aller ni dans le dépôt, ni dans l'inventaire, ni dans les variables ordinaires.
La règle est simple à énoncer : un secret ne va jamais dans Git en clair. Et elle est plus
difficile à tenir qu'il n'y paraît, parce que Git n'oublie pas : un secret commité puis
retiré au commit suivant reste dans l'historique, accessible à quiconque clone le dépôt. Le
retirer vraiment demande de réécrire l'historique — le rebase du chapitre 2, avec toutes ses
conséquences sur les collègues — et, surtout, le secret doit être considéré comme compromis
et donc changé.
Trois mécanismes, du plus simple au plus solide.
Les fichiers chiffrés dans le dépôt : ansible-vault chiffre un fichier de variables avec un
mot de passe. Le fichier est versionné, illisible sans la clé, et la clé est fournie à
l'exécution. C'est suffisant pour un projet d'étudiants, et cela déplace le problème sur une
seule clé à protéger.
Les variables d'environnement injectées par le pipeline : les secrets sont stockés dans le gestionnaire de l'outil d'intégration continue, jamais dans le dépôt.
Les coffres-forts — HashiCorp Vault, gestionnaires de secrets des fournisseurs. Ils ajoutent ce que les deux premiers n'ont pas : journalisation des accès, rotation automatique, et secrets à durée de vie limitée. Un identifiant de base valable une heure et renouvelé automatiquement vaut mieux qu'un mot de passe permanent bien chiffré.
Le chapitre 11 reprendra ce sujet du côté de la détection : comment s'assurer qu'aucun secret n'entre dans le dépôt, plutôt que de le retirer après coup.
Un playbook exécuté deux fois de suite affiche changed=7 la seconde fois. Que faut-il en conclure ?
Un mot de passe a été commité par erreur, puis retiré au commit suivant. Est-ce réglé ?
À vous
L'exercice construit un exécuteur de playbook et mesure l'idempotence.
Vous écrirez le moteur : pour chaque tâche, comparer l'état voulu à l'état de la machine
simulée, agir si nécessaire, et compter ok ou changed. Puis vous exécuterez deux fois le
même playbook — la seconde doit afficher changed=0.
Le jeu de tâches contient délibérément une tâche shell qui rapporte une modification à chaque
exécution. Vous la corrigerez de deux façons — module dédié, puis condition creates — et vous
vérifierez le décompte.
La dernière partie est un détecteur de secrets, appliqué à une série de modifications : clé d'interface, mot de passe de connexion, jeton dans une chaîne. Vous mesurerez aussi les faux positifs, parce qu'un détecteur qui crie trop est désactivé — c'est le bruit d'alertes du chapitre 10, par anticipation.
Écrivez le moteur qui produit le décompte ok/changed, réparez une tâche non idempotente, puis détectez des secrets.
// ── Une machine simulée ─────────────────────────────────────────────────── function creerMachine() { return { paquets: new Set(), fichiers: new Map(), services: new Map() }; } // ── Les modules : chacun COMPARE avant d'agir ───────────────────────────── const MODULES = { package: (m, a) => { if (m.paquets.has(a.name)) return false; // déjà là : rien à faire m.paquets.add(a.name); return true; // changed }, copy: (m, a) => { if (m.fichiers.get(a.dest) === a.contenu) return false; m.fichiers.set(a.dest, a.contenu); return true; }, service: (m, a) => { if (m.services.get(a.name) === a.state) return false; m.services.set(a.name, a.state); return true; }, get_url: (m, a) => { return false; // ← à écrire : ne retélécharge pas si le fichier est là }, // La porte de sortie : Ansible ne peut pas savoir si la commande a agi. shell: (m, a) => { if (a.creates && m.fichiers.has(a.creates)) return false; // ← à honorer return true; // sinon : TOUJOURS changed }, }; function executer(playbook, machine, tracer) { let ok = 0, changed = 0; const gestionnaires = new Set(); for (const t of playbook) { const aChange = MODULES[t.module](machine, t); if (aChange) { changed++; if (t.notify) gestionnaires.add(t.notify); } else ok++; if (tracer) console.log(" " + (aChange ? "changed" : "ok ") + " " + t.name); } // Un gestionnaire ne part QUE si quelque chose a changé, et une seule fois. for (const g of gestionnaires) { if (tracer) console.log(" changed [handler] " + g); changed++; } return { ok, changed }; } const PLAYBOOK = [ { module: "package", name: "nginx", /* … */ }, { module: "copy", dest: "/etc/nginx/nginx.conf", contenu: "server{}", notify: "recharger nginx" }, { module: "service", name: "nginx", state: "started" }, { module: "shell", cmd: "curl -o /opt/outil https://exemple.net/outil" }, ]; PLAYBOOK.forEach((t, i) => { t.name = t.name ?? t.dest ?? t.cmd; }); // ── Détection de secrets ────────────────────────────────────────────────── const MODIFICATIONS = [ 'const cle = "sk_live_4eC39HqLyjWDarjtT1zdp7dc";', 'DATABASE_URL=postgres://app:Tr0ub4dor@db:5432/app', 'const cle = process.env.STRIPE_KEY;', '// exemple : token = "xxxxxxxxxxxx"', 'password_hash = "$2b$12$KIXQ2ei0Rl0Cq0Bl6Xa2yO"', ]; function detecter(ligne) { return false; // ← à écrire, puis à mesurer sur les faux positifs } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Exécutez le playbook DEUX fois. Que vaut changed la seconde fois ? // 2. Écrivez get_url et honorez creates dans shell. Corrigez la tâche // fautive de deux façons, et vérifiez changed=0. // 3. Écrivez detecter(), puis comptez vrais et faux positifs. const m = creerMachine(); console.log(" première exécution :"); console.log(" " + JSON.stringify(executer(PLAYBOOK, m, true)));
En travaux pratiques
Configurer, et mesurer l'idempotence
Écrire un playbook qui configure une machine, prouver son idempotence par le décompte, puis réparer une tâche qui la casse.
- Ansible installé
- Deux conteneurs cibles avec Python, servant de machines de laboratoire
- 1. Monter l'inventaire
Déclarez vos deux conteneurs comme cibles, en connexion locale par Docker plutôt que par SSH, et vérifiez la connectivité avec le module ping.
- 2. Écrire le playbook
Installez nginx, déposez une page à partir d'un gabarit contenant le nom de la machine, et démarrez le service. Ajoutez un gestionnaire qui recharge nginx quand la configuration change.
- 3. Le test qui compte
Exécutez le playbook deux fois de suite. Relevez le décompte final des deux exécutions. Que doit valoir « changed » la seconde fois ?
- 4. Casser l'idempotence
Ajoutez une tâche qui télécharge un fichier avec une commande shell. Réexécutez deux fois et observez le décompte. Expliquez pourquoi Ansible ne peut pas faire mieux.
- 5. La réparer, de deux façons
Corrigez d'abord avec une condition de création, puis en remplaçant la commande par un module dédié. Vérifiez le décompte après chaque correction, et dites laquelle vous préférez.
- 6. Chiffrer un secret
Placez le mot de passe de la base dans un fichier de variables chiffré, versionnez-le, et vérifiez qu'il est illisible sans la clé. Le playbook doit continuer de fonctionner.
- 7. Vérifier le gestionnaire
Modifiez le gabarit et relancez : le gestionnaire doit partir. Relancez sans rien modifier : il ne doit pas partir. Expliquez l'intérêt de ce comportement.
- La seconde exécution affiche changed=0
- Le fichier de variables est illisible dans le dépôt
- Le gestionnaire ne se déclenche que lorsque quelque chose a réellement changé
Ce que la suite en fait
Le bloc V change d'échelle. Ansible configure des machines qu'on connaît par leur nom ; Kubernetes reçoit une description et décide lui-même où placer les charges, en les déplaçant quand une machine tombe. Il n'y a plus de machine à configurer, seulement un état souhaité à déclarer — la boucle de réconciliation du chapitre suivant est ce chapitre poussé à sa limite.
Les secrets, eux, reviendront deux fois : comme objet Kubernetes au chapitre 9, et comme sujet de détection au chapitre 11.
À retenir
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.