C4 — Infrastructure as CodeDans le dialogue d’impression, choisissez « Enregistrer au format PDF » comme destination.
Retour

Licence 3 · DevOps

Cours 4Infrastructure as Code

Décrire une infrastructure dans un fichier versionné plutôt que de la construire à la main, et pouvoir la reconstruire à l'identique.

2 chapitres · 10 h de travail estimé

  1. 1. Provisionnement déclaratif6 h
  2. 2. Configuration4 h

Chapitre 1 · 6 h

Provisionnement déclaratif

Infrastructure immuable ; Terraform : ressources, état, plan et application ; idempotence, modules et réutilisation, gestion de l'état partagé en équipe.

Il existe, dans presque toutes les organisations, un serveur que personne n'ose redémarrer. Il a été installé il y a quatre ans, configuré à la main, ajusté une dizaine de fois par des personnes qui ont depuis changé d'équipe. Il fonctionne. Nul ne sait exactement pourquoi, ni comment le reconstruire si le matériel lâche.

On l'appelle un serveur flocon : unique, et impossible à reproduire. Il est le symptôme d'un problème plus général, la dérive de configuration — l'écart qui se creuse entre ce qu'on croit avoir déployé et ce qui tourne réellement.

Ce chapitre supprime cette catégorie de problème en changeant l'objet qu'on manipule : on ne configure plus des machines, on décrit une infrastructure dans des fichiers versionnés.

Immuable plutôt que réparé

Deux façons de faire évoluer une machine.

La mutation : on se connecte, on met à jour un paquet, on modifie un fichier de configuration. Chaque intervention éloigne un peu plus la machine de son état d'origine, et deux serveurs installés le même jour finissent différents.

L'immuabilité : on ne modifie jamais une machine en service. On construit une nouvelle image, on déploie de nouvelles instances, on retire les anciennes. C'est exactement le conteneur du chapitre 3, à l'échelle de l'infrastructure — et c'est le même déploiement progressif qu'au chapitre 6.

La formule consacrée : on traite les serveurs comme du bétail, pas comme des animaux de compagnie. Un animal de compagnie a un nom, on le soigne quand il est malade. Un troupeau se compte : un animal malade est remplacé.

Trois bénéfices en découlent. Plus de dérive : l'état d'une machine est entièrement déterminé par l'image dont elle vient. Retour arrière trivial : redéployer l'image précédente. Et recette fidèle : le même code produit un environnement identique, ce qui est la condition posée au chapitre 6.

Déclaratif plutôt qu'impératif

C'est le second renversement, et il structure aussi le chapitre 9.

Un script impératif décrit les étapes : créer le réseau, puis la machine, puis la règle de pare-feu. Il suppose un état de départ connu, et devient faux dès que quelque chose existe déjà — il faut alors ajouter partout des « si cela n'existe pas ».

Une description déclarative énonce l'état voulu : il doit exister un réseau, deux machines et une règle. L'outil compare cette description au monde réel et calcule lui-même ce qu'il faut faire — créer, modifier, détruire, ou ne rien faire.

resource "aws_instance" "serveur_web" {  ami           = "ami-0c55b159"  instance_type = "t3.micro"  tags = { Name = "web-production" }} resource "aws_security_group" "web" {  ingress {    from_port   = 443    to_port     = 443    protocol    = "tcp"    cidr_blocks = ["0.0.0.0/0"]  }}

Ce fichier ne dit pas comment créer une machine : il dit qu'il doit en exister une, de ce type, avec ce nom. Le comment appartient à l'outil.

Plan, puis application

Terraform travaille en deux temps, et c'est ce qui le rend utilisable en équipe.

terraform plan     compare la description au monde réel                   et affiche CE QUI VA CHANGER, sans rien changer terraform apply    exécute ce plan

Le plan est l'artefact le plus important du chapitre. Il se lit avant d'agir, il se relit en demande de fusion, et il est le seul moyen de répondre à la question qui compte : « qu'est-ce que cette modification va détruire ? »

Terraform will perform the following actions:   + aws_instance.serveur_web_2      will be created  ~ aws_security_group.web          will be updated in-place  - aws_instance.ancien_serveur     will be destroyed-/+ aws_db_instance.principale      must be REPLACED

La dernière ligne est celle qu'il faut savoir repérer. Certaines modifications ne peuvent pas être appliquées sur place : changer la zone d'une machine, ou un paramètre immuable d'une base, implique de détruire puis recréer. Sur une base de production, c'est la perte des données — et le plan est le seul endroit où l'on peut s'en apercevoir avant.

D'où la règle de conduite : on lit le plan, ligne à ligne, et l'on cherche les - et les -/+. Un apply lancé sans avoir lu son plan est l'équivalent d'un rm -rf sans regarder le chemin.

L'état, et pourquoi c'est le point dur

Pour calculer un plan, Terraform a besoin de savoir ce qu'il a déjà créé. C'est le rôle du fichier d'état : la correspondance entre les ressources décrites dans le code et les objets réels chez le fournisseur.

   description (code)  ←──  état  ──→  monde réel

Trois écarts sont donc possibles, et ils ne se traitent pas de la même façon.

Le code a changé : c'est le cas normal, le plan propose les modifications.

Le monde réel a changé sans passer par Terraform — quelqu'un a modifié une règle depuis la console web. C'est la dérive, et le plan la révèle en proposant de revenir à ce qui est décrit. La bonne réaction n'est pas d'accepter la dérive mais de comprendre pourquoi quelqu'un a eu besoin de contourner le code.

L'état est perdu ou faux : Terraform ne sait plus ce qui lui appartient. Il proposera de recréer des ressources qui existent déjà, ce qui échouera — ou pire, réussira en double. C'est le scénario catastrophe du chapitre.

D'où trois règles, non négociables. L'état ne va jamais dans Git : il contient des valeurs sensibles en clair — mots de passe générés, clés — et deux personnes qui le versionnent produisent des conflits inextricables. Il vit dans un stockage distant partagé. Et ce stockage doit fournir un verrou : sans lui, deux apply simultanés écrivent chacun leur version de l'état, et la dernière écriture efface l'autre. On se retrouve avec des ressources créées que plus rien ne référence — invisibles, facturées, et impossibles à détruire proprement.

Idempotence et modules

L'idempotence est la propriété qui rend tout cela sûr : appliquer deux fois produit le même résultat qu'appliquer une fois. Puisque l'outil compare l'état voulu au réel, un second apply sans modification ne fait rien — No changes. C'est ce qui permet de relancer sans crainte après une interruption, et c'est ce que le chapitre 8 retrouvera avec Ansible.

Les modules évitent la duplication : un module « environnement applicatif » paramétré par le nom et la taille, instancié trois fois pour le développement, la recette et la production. C'est la même motivation que la fonction en programmation — et le même piège, un module trop paramétré devenant plus difficile à comprendre que le code qu'il factorise.

Quiz · 1 question

Un plan Terraform affiche « -/+ aws_db_instance.principale must be replaced ». Que signifie cette ligne ?

  • La base sera mise à jour sur place, avec un bref redémarragemise à jour sur place
  • La base sera DÉTRUITE puis RECRÉÉE, parce que la modification demandée porte sur un paramètre immuable — sur une base de production, cela signifie la perte des donnéesdestruction puis recréation
  • Terraform hésite entre deux actions et demandera confirmation pendant l'applicationdemande de confirmation

Réponse : Le symbole -/+ signale un remplacement : certains attributs ne peuvent pas être modifiés sur une ressource existante — la zone de disponibilité d'une machine, le moteur d'une base, parfois un simple nom — et l'outil n'a d'autre choix que de détruire puis recréer. Sur une base de données de production, cela veut dire perdre les données, et l'application se fera sans autre avertissement que cette ligne : il n'y a PAS de confirmation interactive par ressource. C'est précisément pourquoi le plan existe et pourquoi on le lit ligne à ligne, en cherchant les - et les -/+. Un apply lancé sans avoir lu son plan est l'équivalent d'un rm -rf sans regarder le chemin. Les parades : lire le plan en demande de fusion, et poser un verrou de protection contre la destruction sur les ressources critiques.

Quiz · 1 question

Deux ingénieurs lancent terraform apply en même temps sur un état stocké dans un simple fichier partagé, sans verrou. Que se passe-t-il ?

  • Le second attend automatiquement la fin du premier : Terraform sérialise les applicationssérialisation automatique
  • Chacun part du même état initial et écrit le sien à la fin : la dernière écriture écrase l'autre, et des ressources créées par le premier ne sont plus référencées — invisibles, facturées, impossibles à détruire proprementétat écrasé
  • Les deux applications échouent, ce qui est sans conséquencedouble échec

Réponse : Rien ne sérialise quoi que ce soit : la sérialisation vient du VERROU, et c'est le stockage distant qui le fournit, pas Terraform seul. Sans lui, les deux processus lisent le même état de départ, calculent chacun leur plan, créent chacun leurs ressources, puis écrivent chacun leur version finale de l'état. La seconde écriture écrase la première, et tout ce que le premier avait créé disparaît de l'état — mais pas du monde réel. On obtient des ressources orphelines : elles existent, elles sont facturées, aucun code ne les décrit, et un terraform destroy ne les touchera pas. Les retrouver demande de comparer à la main l'inventaire du fournisseur et l'état. D'où les trois règles : l'état ne va jamais dans Git (il contient des secrets en clair et produit des conflits inextricables), il vit dans un stockage distant partagé, et ce stockage fournit un verrou.

À vous

L'exercice construit un Terraform miniature, et c'est le seul moyen de voir ce que l'outil fait réellement.

Vous écrirez le calcul du plan : comparer la description, l'état et le monde réel, et en déduire les créations, modifications, remplacements et destructions. Le jeu de données contient un attribut immuable, pour que le -/+ apparaisse et qu'il faille le repérer.

Ensuite l'idempotence : appliquer, puis réappliquer, et vérifier qu'il ne se passe plus rien. Puis la dérive : quelqu'un modifie le monde réel sans passer par le code, et vous verrez le plan proposer de revenir en arrière.

La dernière partie simule deux applications concurrentes, avec et sans verrou. Vous compterez les ressources orphelines produites dans le premier cas.

Exercice de code

Calculez un plan avec remplacements et destructions, vérifiez l'idempotence, puis provoquez une dérive et une course.

Point de départ

// ── Trois sources de vérité ───────────────────────────────────────────────
const IMMUABLES = ["zone", "moteur"];   // modifier cela impose un remplacement

// Ce que le code décrit.
const CODE = {
  "instance.web":     { type: "t3.small", zone: "eu-west-1a" },
  "instance.web2":    { type: "t3.micro", zone: "eu-west-1b" },
  "securite.web":     { port: 443 },
  "base.principale":  { taille: 100, moteur: "postgres16" },
};

// Ce que l'outil croit avoir créé.
const ETAT = {
  "instance.web":     { type: "t3.micro", zone: "eu-west-1a" },
  "securite.web":     { port: 443 },
  "base.principale":  { taille: 100, moteur: "postgres15" },
  "instance.ancienne": { type: "t3.micro", zone: "eu-west-1a" },
};

// Ce qui existe réellement chez le fournisseur.
const REEL = { ...ETAT };

function plan(code, etat) {
  const actions = [];
  for (const [nom, voulu] of Object.entries(code)) {
    const connu = etat[nom];
    if (!connu) { actions.push({ signe: "+", nom, quoi: "sera créée" }); continue; }
    const differents = Object.keys(voulu).filter((k) => voulu[k] !== connu[k]);
    if (differents.length === 0) continue;
    // ← à écrire : si un attribut IMMUABLE change, c'est un remplacement
    actions.push({ signe: "~", nom, quoi: "modifiée sur place : " + differents.join(", ") });
  }
  // ← à écrire : ce qui est dans l'état mais plus dans le code sera détruit
  return actions;
}

function afficher(actions) {
  if (!actions.length) { console.log("      No changes. Infrastructure is up-to-date."); return; }
  for (const a of actions) {
    console.log("      " + a.signe.padStart(3) + " " + a.nom.padEnd(20) + a.quoi);
  }
}

// ── À VOUS ────────────────────────────────────────────────────────────────
// 1. Complétez plan() : remplacement sur attribut immuable, et destruction.
//    Repérez la ligne -/+ : que se passerait-il si l'on appliquait ?
// 2. Appliquez, puis replanifiez : vérifiez l'idempotence.
// 3. Simulez une DÉRIVE — quelqu'un modifie le monde réel depuis la console.
// 4. Simulez deux applications concurrentes sans verrou, puis avec.

afficher(plan(CODE, ETAT));

Solution

const IMMUABLES = ["zone", "moteur"];

const CODE = {
  "instance.web":    { type: "t3.small", zone: "eu-west-1a" },
  "instance.web2":   { type: "t3.micro", zone: "eu-west-1b" },
  "securite.web":    { port: 443 },
  "base.principale": { taille: 100, moteur: "postgres16" },
};
const ETAT_INITIAL = {
  "instance.web":      { type: "t3.micro", zone: "eu-west-1a" },
  "securite.web":      { port: 443 },
  "base.principale":   { taille: 100, moteur: "postgres15" },
  "instance.ancienne": { type: "t3.micro", zone: "eu-west-1a" },
};

function plan(code, etat) {
  const actions = [];
  for (const [nom, voulu] of Object.entries(code)) {
    const connu = etat[nom];
    if (!connu) { actions.push({ signe: "+", nom, quoi: "sera créée" }); continue; }
    const differents = Object.keys(voulu).filter((k) => voulu[k] !== connu[k]);
    if (differents.length === 0) continue;
    const immuablesTouches = differents.filter((k) => IMMUABLES.includes(k));
    if (immuablesTouches.length) {
      // Un attribut immuable ne se modifie pas : il faut détruire et recréer.
      actions.push({ signe: "-/+", nom,
        quoi: "DOIT ÊTRE REMPLACÉE (attribut immuable : " + immuablesTouches.join(", ") + ")" });
    } else {
      actions.push({ signe: "~", nom, quoi: "modifiée sur place : " + differents.join(", ") });
    }
  }
  for (const nom of Object.keys(etat)) {
    if (!code[nom]) actions.push({ signe: "-", nom, quoi: "sera détruite" });
  }
  return actions;
}

function afficher(actions) {
  if (!actions.length) { console.log("      No changes. Infrastructure is up-to-date."); return; }
  for (const a of actions) console.log("      " + a.signe.padStart(3) + " " + a.nom.padEnd(20) + a.quoi);
}

function appliquer(code, etat) {
  const nouveau = {};
  for (const [nom, voulu] of Object.entries(code)) nouveau[nom] = { ...voulu };
  return nouveau;   // après application, l'état reflète le code
}

console.log("— 1. le plan —");
afficher(plan(CODE, ETAT_INITIAL));
console.log("      ↑ La ligne -/+ est celle à repérer : appliquer ce plan DÉTRUIRAIT");
console.log("        la base de production et la recréerait vide. Il n'y a aucune");
console.log("        confirmation par ressource — la lecture du plan est la seule");
console.log("        protection, avec le verrou de prévention de destruction.");

console.log("");
console.log("— 2. idempotence —");
let etat = appliquer(CODE, ETAT_INITIAL);
console.log("   après application, second plan :");
afficher(plan(CODE, etat));
console.log("      Appliquer deux fois produit le même résultat qu'appliquer une");
console.log("      fois : c'est ce qui permet de relancer sans crainte après une");
console.log("      interruption.");

console.log("");
console.log("— 3. dérive : quelqu'un a modifié le monde réel à la main —");
const reel = { ...etat, "securite.web": { port: 22 } };   // un port SSH ouvert en console
console.log("   plan calculé contre le RÉEL observé :");
afficher(plan(CODE, reel));
console.log("      Le plan propose de revenir à ce que le code décrit. La bonne");
console.log("      réaction n'est pas d'accepter la dérive en modifiant le code,");
console.log("      mais de comprendre POURQUOI quelqu'un a eu besoin de contourner.");

console.log("");
console.log("— 4. deux applications concurrentes —");
function concurrent(avecVerrou) {
  const depart = { "securite.web": { port: 443 } };
  const codeA = { ...depart, "instance.a": { type: "t3.micro", zone: "eu-west-1a" } };
  const codeB = { ...depart, "instance.b": { type: "t3.micro", zone: "eu-west-1b" } };
  const reelFinal = { ...depart, "instance.a": codeA["instance.a"], "instance.b": codeB["instance.b"] };

  let etatFinal;
  if (avecVerrou) {
    // Sérialisé : B part de l'état laissé par A.
    const apresA = appliquer(codeA, depart);
    etatFinal = appliquer({ ...codeB, ...apresA }, apresA);
  } else {
    // Les deux partent du MÊME état ; la dernière écriture gagne.
    appliquer(codeA, depart);
    etatFinal = appliquer(codeB, depart);
  }
  const orphelines = Object.keys(reelFinal).filter((r) => !etatFinal[r]);
  return { etatFinal: Object.keys(etatFinal), orphelines };
}
for (const avec of [false, true]) {
  const r = concurrent(avec);
  console.log("   " + (avec ? "avec verrou  " : "sans verrou  ") +
    "état final : [" + r.etatFinal.join(", ") + "]");
  console.log("      ressources orphelines : " +
    (r.orphelines.length ? r.orphelines.join(", ") + "  ← existent, facturées, décrites par aucun code"
                         : "aucune"));
}
console.log("   Sans verrou, ce que le premier a créé disparaît de l'état mais pas");
console.log("   du monde réel. Un destroy ne les touchera pas : il faut comparer à");
console.log("   la main l'inventaire du fournisseur et l'état pour les retrouver.");

En travaux pratiques

Travaux pratiques 7 · 3 h

Décrire l'infrastructure, sans un centime d'hébergement

Écrire, planifier et appliquer une infrastructure réelle avec Terraform — en pilotant Docker en local, ce qui donne un vrai état, une vraie dérive et un vrai verrou.

Avant de commencer

  • Terraform ou OpenTofu installé
  • Docker en fonctionnement
  • Les images du TP 3 disponibles localement

Énoncé

  1. Décrire la pileAvec le fournisseur Docker, décrivez un réseau, un volume, un conteneur de base et un conteneur applicatif. Lancez un plan et LISEZ-LE avant d'appliquer. Indice : Le fournisseur kreuzwerker/docker expose docker_network, docker_volume, docker_image et docker_container.
  2. Éprouver l'idempotenceAppliquez, puis replanifiez sans rien modifier. Notez ce qu'affiche le plan, et expliquez pourquoi.
  3. Provoquer une dériveArrêtez un conteneur à la main avec Docker, sans passer par Terraform. Replanifiez. Que propose l'outil, et pourquoi ?
  4. Repérer un remplacementModifiez un attribut modifiable sur place, puis un attribut immuable. Comparez les deux plans et repérez le symbole qui distingue une modification d'un remplacement. Indice : Changer le nom d'un conteneur ne se fait pas sur place.
  5. Factoriser en moduleTransformez la description en module paramétré par le nom de l'environnement et le nombre d'exemplaires, puis instanciez-le deux fois : recette et production.
  6. Casser l'état, et le réparerFaites une copie de sauvegarde du fichier d'état, puis supprimez-le. Replanifiez. Que propose Terraform ? Restaurez ensuite la sauvegarde.
  7. La courseÀ deux, sur le même état partagé sans verrou, lancez une application simultanément. Comparez ensuite le contenu de l'état et la liste réelle des conteneurs.

C'est réussi quand

  • Un second plan sans modification affiche « no changes »
  • Vous savez repérer un remplacement dans un plan et dire ce qu'il détruirait
  • Le module s'instancie deux fois sans duplication de code
  • Vous pouvez nommer au moins une ressource orpheline produite par la course

Correction

La descriptionmain.tf
terraform {
required_providers {
  docker = { source = "kreuzwerker/docker", version = "~> 3.0" }
}
}

resource "docker_network" "interne" {
name = "tickets-interne"
}

resource "docker_volume" "donnees" {
name = "tickets-donnees"
}

resource "docker_container" "base" {
name  = "tickets-base"
image = "postgres:16-alpine"
env   = ["POSTGRES_USER=app", "POSTGRES_PASSWORD=secret"]
networks_advanced { name = docker_network.interne.name }
volumes {
  volume_name    = docker_volume.donnees.name
  container_path = "/var/lib/postgresql/data"
}
}

Aucune de ces lignes ne dit COMMENT créer quoi que ce soit : elles disent ce qui doit exister. L'outil compare cette description au monde réel et en déduit les actions — c'est tout le déclaratif.

Les quatre plans, et ce qu'ils apprennent
# après application, sans modification
No changes. Your infrastructure matches the configuration.

# après avoir arrêté un conteneur à la main
~ docker_container.base will be updated in-place
  ~ must_run: false -> true

# attribut modifiable
~ docker_container.appli will be updated in-place

# attribut immuable (le nom)
-/+ docker_container.appli must be replaced

Le second plan est la DÉRIVE : le monde réel a changé sans passer par le code, et l'outil propose de revenir à ce qui est décrit. La bonne réaction n'est pas de modifier le code pour entériner la dérive, mais de comprendre pourquoi quelqu'un a eu besoin de contourner.

Le modulemodules/pile/variables.tf
variable "environnement" { type = string }
variable "exemplaires"   { type = number, default = 1 }

# racine
module "recette" {
source        = "./modules/pile"
environnement = "recette"
}

module "production" {
source        = "./modules/pile"
environnement = "production"
exemplaires   = 3
}

Le même code décrit les deux environnements, et c'est ce qui rend la recette FIDÈLE à la production — condition posée au chapitre 6. Attention toutefois au module trop paramétré : au-delà d'une dizaine de variables, il devient plus difficile à comprendre que le code qu'il factorise.

L'état perdu
# après suppression de terraform.tfstate
Plan: 4 to add, 0 to change, 0 to destroy.

Error: container name "tickets-base" is already in use

Terraform ne sait plus ce qui lui appartient : il propose de tout recréer, et échoue parce que les objets existent déjà. C'est le scénario catastrophe, et il explique les trois règles — l'état ne va jamais dans Git (il contient des secrets en clair), il vit dans un stockage distant partagé, et ce stockage fournit un verrou.

Le résultat de la course
# A crée appli-1, B crée appli-2, les deux partis du même état
docker ps            # appli-1 ET appli-2 existent
terraform state list # seul appli-2 figure  <-- appli-1 est ORPHELIN

terraform destroy    # ne détruit pas appli-1 : il l'ignore

La dernière écriture de l'état a écrasé l'autre. Le conteneur créé par le premier existe, tourne, consomme — et aucun code ne le décrit. Sur un fournisseur de nuage, il serait facturé, et le retrouver demanderait de comparer à la main l'inventaire et l'état.

Ce que la suite en fait

Le chapitre 8 traite ce que celui-ci laisse de côté. Terraform crée une machine ; il ne dit pas ce qui tourne dessus. Ansible installe, configure, déploie — et l'on verra que la frontière entre les deux est moins nette qu'il n'y paraît, l'infrastructure immuable réduisant beaucoup le besoin de configuration.

Le chapitre 9 poussera le déclaratif jusqu'au bout : Kubernetes est une boucle de réconciliation permanente là où Terraform en exécute une à la demande. C'est la même idée, avec un contrôleur qui ne s'arrête jamais.

À retenir

Flashcards · 5 cartes

Qu'est-ce qu'un serveur flocon, et que propose l'infrastructure immuable ?
Une machine configurée à la main pendant des années, que plus personne ne sait reconstruire — symptôme de la DÉRIVE DE CONFIGURATION, l'écart entre ce qu'on croit avoir déployé et ce qui tourne. L'immuabilité y répond : on ne modifie jamais une machine en service, on construit une NOUVELLE image, on déploie, on retire l'ancienne. Formule : des serveurs traités comme du bétail, pas comme des animaux de compagnie. Bénéfices : plus de dérive, retour arrière trivial, recette fidèle.
Opposez impératif et déclaratif.
L'IMPÉRATIF décrit LES ÉTAPES — créer le réseau, puis la machine — ce qui suppose un état de départ connu et devient faux dès que quelque chose existe déjà. Le DÉCLARATIF énonce L'ÉTAT VOULU — il doit exister un réseau et deux machines — et l'outil compare au monde réel puis calcule lui-même quoi créer, modifier, détruire ou laisser. Le « comment » appartient à l'outil, et c'est aussi le modèle de Kubernetes.
Pourquoi le plan est-il l'artefact le plus important, et que faut-il y chercher ?
Parce qu'il montre CE QUI VA CHANGER sans rien changer : il se lit avant d'agir, se relit en demande de fusion, et répond à la seule question qui compte — qu'est-ce que cette modification va détruire ? On y cherche les « - » (destruction) et surtout les « -/+ » (REMPLACEMENT) : certains attributs ne se modifient pas sur place, et sur une base de production cela signifie perdre les données. Un apply lancé sans avoir lu son plan est un rm -rf sans regarder le chemin.
À quoi sert le fichier d'état, et quels écarts révèle-t-il ?
Il fait la correspondance entre les ressources DÉCRITES dans le code et les objets RÉELS chez le fournisseur — sans lui, aucun plan n'est calculable. Trois écarts : le CODE a changé (cas normal) ; le MONDE RÉEL a changé sans passer par l'outil, c'est la DÉRIVE, et le plan propose de revenir au code — la bonne réaction est de comprendre pourquoi quelqu'un a contourné ; l'ÉTAT est perdu ou faux, et l'outil ne sait plus ce qui lui appartient.
Quelles sont les trois règles sur l'état, et que risque-t-on sans verrou ?
1) IL NE VA JAMAIS DANS GIT : il contient des valeurs sensibles en clair et produit des conflits inextricables. 2) Il vit dans un STOCKAGE DISTANT PARTAGÉ. 3) Ce stockage fournit un VERROU. Sans verrou, deux apply simultanés partent du même état, créent chacun leurs ressources, et la dernière écriture écrase l'autre : les ressources du premier deviennent ORPHELINES — existantes, facturées, décrites par aucun code, et qu'un destroy ne touchera pas.

Chapitre 2 · 4 h

Configuration

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 · 1 question

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

  • C'est normal : Ansible réapplique la configuration à chaque exécutioncomportement normal
  • Le playbook n'est pas idempotent — probablement des tâches shell ou command, qui déclarent changed à chaque fois. On ne peut plus distinguer « la configuration a dérivé » de « ce playbook ment »playbook non idempotent
  • Les machines cibles ont redémarré entre les deux exécutionsredémarrage

Réponse : L'idempotence est la propriété centrale de la gestion de configuration : les tâches déclarent un ÉTAT — « nginx doit être installé » — et un module bien écrit vérifie d'abord si l'état est déjà atteint, auquel cas il rapporte ok. Une seconde exécution sans modification doit donc afficher changed=0, et ce test devrait faire partie du pipeline. Sept modifications signalées alors que rien n'a bougé viennent presque toujours des modules command et shell : Ansible ne peut pas savoir si la commande a changé quelque chose, donc il déclare changed systématiquement. Le coût n'est pas cosmétique — le décompte est le seul indicateur de dérive dont on dispose, et un playbook bruyant le rend inutilisable. Trois remèdes : employer un module dédié (get_url plutôt que curl), déclarer creates: pour faire sauter la tâche, ou préciser changed_when.

Quiz · 1 question

Un mot de passe a été commité par erreur, puis retiré au commit suivant. Est-ce réglé ?

  • Oui : le fichier actuel ne le contient plus, donc il n'est plus exposéréglé
  • Non : Git n'oublie pas, le secret reste dans l'historique et quiconque clone le dépôt peut le lire. Il faut le considérer comme COMPROMIS et le changer — réécrire l'historique ne suffit pascompromis, à changer
  • Non, mais il suffit de réécrire l'historique avec un rebase pour effacer toute tracerebase suffisant

Réponse : Un dépôt conserve tous les états successifs : le commit qui contenait le secret existe toujours, et il suffit de remonter l'historique pour le lire. Cela vaut pour tout clone déjà fait, pour les copies chez les collègues, pour les caches des serveurs d'intégration continue et pour les miroirs. Réécrire l'historique — avec les conséquences du chapitre 2 sur les branches partagées — retire le secret du dépôt d'origine, mais ne le retire de nulle part ailleurs, et il a pu être aspiré entre-temps : les dépôts publics sont scrutés en permanence par des robots, et une clé publiée y est utilisée en quelques minutes. La seule action qui règle réellement le problème est la ROTATION : considérer le secret comme compromis et le remplacer. Le nettoyage de l'historique vient après, et le chapitre 11 traitera de l'empêcher d'entrer.

À 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 de code

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

Point de départ

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

Solution

function creerMachine() {
  return { paquets: new Set(), fichiers: new Map(), services: new Map() };
}

const MODULES = {
  package: (m, a) => { if (m.paquets.has(a.name)) return false; m.paquets.add(a.name); return true; },
  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;
  },
  // Un module dédié COMPARE avant d'agir : c'est toute la différence.
  get_url: (m, a) => {
    if (m.fichiers.has(a.dest)) return false;
    m.fichiers.set(a.dest, "téléchargé"); return true;
  },
  shell: (m, a) => {
    if (a.creates && m.fichiers.has(a.creates)) return false;
    if (a.creates) m.fichiers.set(a.creates, "créé par shell");
    return true;
  },
};

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);
  }
  for (const g of gestionnaires) { if (tracer) console.log("      changed  [handler] " + g); changed++; }
  return { ok, changed };
}

const nommer = (p) => p.map((t) => ({ ...t, name: t.name ?? t.dest ?? t.cmd }));

const FAUTIF = nommer([
  { 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" },
]);
const CORRIGE_CREATES = nommer([
  ...FAUTIF.slice(0, 3),
  { module: "shell", cmd: "curl -o /opt/outil …", creates: "/opt/outil" },
]);
const CORRIGE_MODULE = nommer([
  ...FAUTIF.slice(0, 3),
  { module: "get_url", dest: "/opt/outil", name: "télécharger l'outil (get_url)" },
]);

console.log("— 1 et 2. idempotence —");
for (const [nom, pb] of [["playbook fautif (shell)", FAUTIF],
                          ["corrigé par creates:", CORRIGE_CREATES],
                          ["corrigé par get_url", CORRIGE_MODULE]]) {
  const m = creerMachine();
  const un = executer(pb, m, false);
  const deux = executer(pb, m, false);
  console.log("   " + nom.padEnd(26) +
    "1re : ok=" + un.ok + " changed=" + un.changed +
    "   |   2e : ok=" + deux.ok + " changed=" + deux.changed +
    (deux.changed === 0 ? "   idempotent" : "   NON IDEMPOTENT"));
}
console.log("");
console.log("   Détail de la seconde exécution du playbook corrigé :");
const m2 = creerMachine();
executer(CORRIGE_MODULE, m2, false);
executer(CORRIGE_MODULE, m2, true);
console.log("   Le gestionnaire n'est pas parti : rien n'avait changé. C'est ce qui");
console.log("   évite de recharger nginx à chaque exécution — et ce qui rend le");
console.log("   décompte utilisable comme indicateur de dérive.");

console.log("");
console.log("— 3. détection de secrets —");
const MODIFICATIONS = [
  { ligne: 'const cle = "sk_live_4eC39HqLyjWDarjtT1zdp7dc";', secret: true },
  { ligne: 'DATABASE_URL=postgres://app:Tr0ub4dor@db:5432/app', secret: true },
  { ligne: 'const cle = process.env.STRIPE_KEY;', secret: false },
  { ligne: '// exemple : token = "xxxxxxxxxxxx"', secret: false },
  { ligne: 'password_hash = "$2b$12$KIXQ2ei0Rl0Cq0Bl6Xa2yO"', secret: false },
  { ligne: 'AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCY', secret: true },
];

function detecter(ligne) {
  // Préfixes connus de fournisseurs : très fiables, peu de faux positifs.
  if (/\b(sk_live|ghp_|AKIA)[A-Za-z0-9_]{10,}/.test(ligne)) return true;
  // Mot de passe dans une chaîne de connexion.
  if (/:\/\/[^:\s]+:[^@\s]{6,}@/.test(ligne)) return true;
  // Affectation d'une variable au nom sensible avec une valeur longue…
  if (/(SECRET|PASSWORD|TOKEN|API_KEY)[A-Z_]*\s*[=:]\s*["']?[A-Za-z0-9\/+_-]{16,}/i.test(ligne)) {
    // …sauf si c'est une référence à une variable d'environnement, un
    // exemple manifeste, ou une empreinte déjà hachée.
    if (/process\.env|os\.environ|\bxxxx|\$2[aby]\$/.test(ligne)) return false;
    return true;
  }
  return false;
}

let vp = 0, fp = 0, fn = 0;
for (const m of MODIFICATIONS) {
  const d = detecter(m.ligne);
  if (d && m.secret) vp++;
  if (d && !m.secret) fp++;
  if (!d && m.secret) fn++;
  console.log("   " + (d ? "ALERTE " : "       ") + (m.secret ? "[vrai secret] " : "[inoffensif]  ") +
              m.ligne.slice(0, 52));
}
console.log("   vrais positifs " + vp + " | faux positifs " + fp + " | manqués " + fn);
console.log("   Les faux positifs comptent autant que les manqués : un détecteur");
console.log("   qui crie sur chaque hachage et chaque exemple est désactivé au bout");
console.log("   d'une semaine, et ne protège plus de rien. C'est déjà le bruit");
console.log("   d'alertes du chapitre 10.");

En travaux pratiques

Travaux pratiques 8 · 2 h

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.

Avant de commencer

  • Ansible installé
  • Deux conteneurs cibles avec Python, servant de machines de laboratoire

Énoncé

  1. Monter l'inventaireDé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. Indice : ansible_connection: docker évite d'installer un serveur SSH dans les cibles.
  2. Écrire le playbookInstallez 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 compteExé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'idempotenceAjoutez 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çonsCorrigez 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 secretPlacez 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 gestionnaireModifiez 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é

Correction

Inventaire et playbookinventaire.yml / site.yml
# inventaire.yml
web:
hosts:
  labo1: { ansible_connection: docker }
  labo2: { ansible_connection: docker }

# site.yml
- hosts: web
tasks:
  - name: installer nginx
    ansible.builtin.package: { name: nginx, state: present }

  - name: déposer la page
    ansible.builtin.template:
      src: index.html.j2
      dest: /usr/share/nginx/html/index.html
      mode: "0644"
    notify: recharger nginx

  - name: démarrer le service
    ansible.builtin.service: { name: nginx, state: started }

handlers:
  - name: recharger nginx
    ansible.builtin.service: { name: nginx, state: reloaded }

Chaque tâche déclare un ÉTAT — « nginx doit être installé » — et non une commande. Un module bien écrit vérifie d'abord si l'état est atteint, et ne rapporte changed que s'il a agi.

Les deux décomptes
PLAY RECAP (1re exécution)
labo1 : ok=3  changed=3   labo2 : ok=3  changed=3

PLAY RECAP (2e exécution)
labo1 : ok=3  changed=0   labo2 : ok=3  changed=0

Ce zéro est le test d'idempotence, et il devrait figurer dans le pipeline du TP 5. Sans lui, on ne peut plus distinguer « la configuration a dérivé » de « ce playbook ment ».

La tâche qui casse tout, et ses deux corrections
# NON idempotent : changed à chaque exécution
- ansible.builtin.shell: curl -o /opt/outil https://exemple.net/outil

# correction 1 : condition de création
- ansible.builtin.shell: curl -o /opt/outil https://exemple.net/outil
args: { creates: /opt/outil }

# correction 2, préférable : un module dédié
- ansible.builtin.get_url:
  url: https://exemple.net/outil
  dest: /opt/outil
  mode: "0755"

Ansible ne peut pas savoir si une commande arbitraire a changé quelque chose : il déclare donc changed systématiquement. La seconde correction est meilleure — get_url gère aussi les droits, la somme de contrôle et le remplacement partiel. shell est l'aveu qu'aucun module ne convient, et chaque occurrence est une tâche dont on ne sait plus si elle est sûre à relancer.

Le secret chiffréterminal
ansible-vault create group_vars/web/secrets.yml
# on y écrit :  mot_de_passe_base: secret

git add group_vars/web/secrets.yml     # versionné, illisible
cat group_vars/web/secrets.yml         # $ANSIBLE_VAULT;1.1;AES256 …

ansible-playbook site.yml --ask-vault-pass

Le problème est déplacé sur une seule clé à protéger, ce qui est déjà beaucoup mieux qu'un mot de passe en clair. Un coffre-fort ajouterait ce qui manque encore : journalisation des accès, rotation, et surtout secrets à durée de vie limitée.

Pourquoi le gestionnaire compte

Il ne se déclenche QUE si une tâche a réellement changé quelque chose, et une seule fois même si trois tâches le notifient. Sans ce mécanisme, on rechargerait nginx à chaque exécution — inutile, et sur un service à fort trafic, ce n'est pas gratuit. C'est aussi ce qui rend le décompte lisible.

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 · 5 cartes

Qu'est-ce qui distingue provisionnement et gestion de configuration, et comment la frontière a-t-elle bougé ?
Le PROVISIONNEMENT répond à « qu'est-ce qui EXISTE ? » — machines, réseaux, bases. La CONFIGURATION répond à « qu'y a-t-il DESSUS ? » — paquets, fichiers, services, comptes. La frontière s'est déplacée avec l'infrastructure immuable : la configuration est désormais CUITE DANS L'IMAGE à la construction, et Ansible sert à construire l'image plutôt qu'à entretenir des serveurs. Il reste employé pour le matériel physique, les tâches ponctuelles d'exploitation, et les appareils qui ne sont pas des serveurs.
Quelles sont les quatre notions d'Ansible, et quelle absence le caractérise ?
L'INVENTAIRE liste et regroupe les machines. Le PLAYBOOK associe des groupes à des tâches. Les TÂCHES déclarent un ÉTAT (state: present = « il doit être installé »), pas une commande. Les RÔLES regroupent tâches, fichiers et variables en unité réutilisable. L'ABSENCE : rien à installer sur les cibles — connexion SSH, dépôt d'un module, exécution, retour. Pas d'agent, pas de service à maintenir : c'est la raison principale de son adoption. À noter aussi les GESTIONNAIRES, exécutés une seule fois à la fin et seulement si quelque chose a changé.
Comment mesure-t-on l'idempotence, et qu'est-ce qui la casse ?
Par le décompte de fin : ok pour les tâches qui n'ont rien eu à faire, changed pour les autres. LA SECONDE EXÉCUTION DU MÊME PLAYBOOK DOIT AFFICHER changed=0, et ce test devrait être dans le pipeline. Ce qui la casse : les modules command et shell, dont Ansible ne peut pas savoir s'ils ont changé quelque chose — ils déclarent donc changed à chaque fois. Le coût n'est pas cosmétique : le décompte est le seul indicateur de dérive dont on dispose.
Comment rétablir l'idempotence d'une tâche shell ?
Par ordre de préférence : 1) EMPLOYER UN MODULE DÉDIÉ — get_url plutôt que curl, il ne retéléchargera pas si le fichier est là. 2) DÉCLARER creates: — la tâche saute si le fichier existe. 3) En dernier recours, PRÉCISER changed_when à partir de la sortie de la commande. Règle : shell est l'aveu qu'aucun module ne convient ; parfois vrai, cela doit rester rare — chaque occurrence est une tâche dont on ne sait plus si elle est sûre à relancer.
Un secret a été commité puis retiré : que faut-il faire, et pourquoi ?
LE CONSIDÉRER COMME COMPROMIS ET LE CHANGER. Git conserve tous les états : le commit fautif existe toujours, lisible par quiconque a cloné — collègues, caches d'intégration continue, miroirs. Réécrire l'historique le retire du dépôt d'origine et de nulle part ailleurs, et il a pu être aspiré entre-temps : les dépôts publics sont scrutés en permanence, une clé publiée est utilisée en quelques minutes. Le nettoyage vient APRÈS la rotation. Trois mécanismes de stockage : fichiers chiffrés (ansible-vault), variables injectées par le pipeline, et coffres-forts — seuls ces derniers apportent journalisation des accès, rotation et durée de vie limitée.