cursus.

Cours 4 · Infrastructure as CodeLeçon 2 sur 2

Configuration

4 h de lecture8 sections Version PDF

À la fin de cette leçon, vous saurez

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)
Objetmachines, réseaux, bases, DNSpaquets, fichiers, services, comptes
Questionqu'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: reloaded

Les 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=0

La 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/outil

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

Quiz · vérifiez votre compréhension Sans réponse

Un playbook exécuté deux fois de suite affiche changed=7 la seconde fois. Que faut-il en conclure ?

Quiz · vérifiez votre compréhension Sans réponse

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.

Exercice · JavaScript · à vous de jouer

Écrivez le moteur qui produit le décompte ok/changed, réparez une tâche non idempotente, puis détectez des secrets.

En attente
// ── 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)));

Console de sortie
Le résultat s'affiche dans la console

En travaux pratiques

Travaux pratiques 8 · sur machine

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.

2 h
Avant de commencer
  • Ansible installé
  • Deux conteneurs cibles avec Python, servant de machines de laboratoire
  1. 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. 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. 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. 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. 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. 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. 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.

C'est réussi quand
  • 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

Flashcards · 1 / 5Toucher pour retourner
Fin de la leçon

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.