Tous les articles

Tous les articles

Mettre un environnement en veille, et le relancer sans intervention

Ramener le staging à ce qu'une reconstruction réutilise en 87 secondes, puis le relancer seul en onze minutes environ, chaque plan lu par un garde-fou.

Le staging descend désormais jusqu'à son socle en une commande et remonte avec une autre. Le 2026-09-16, une mise en veille a pris 87 secondes et la remontée qui a suivi 677, depuis un environnement réduit à peu près à un projet, deux buckets, une image de noeud et quelques secrets, jusqu'à un cluster Kubernetes dont l'application racine d'Argo CD se déclarait synchronisée et en bonne santé. Personne n'a rien tapé entre les deux.

Le staging est un environnement complet : des noeuds de control plane, une base de données managée, un load balancer et une passerelle, facturés que quelqu'un teste dessus ou non. L'éteindre quand personne n'y travaille arrête cette dépense, et ça ne vaut la peine que si le rallumer ne coûte presque rien.

La première mise en veille était une liste#

La première fois que j'ai mis le staging en veille, le 2026-09-13, c'était une liste de destroys ciblés construite à la main, plus quatre réparations faites en dehors d'OpenTofu. Ça a fonctionné, et ça m'a appris ce qu'une telle liste fait mal. Elle nomme ce qui doit partir, donc tout ce qu'on ajoute ensuite à une racine survit par omission et continue d'être facturé. Elle ne garde aucune trace de la raison de chaque étape, donc la fois suivante répète les mêmes surprises. Et elle demande à quelqu'un de retaper la même séquence minutieuse à chaque fois, ce qui est exactement la manière dont une séquence minutieuse cesse de l'être.

La remontée était pire. Le runbook avait sa propre liste, dont deux variables à passer en ligne de commande et une étape de clôture qui retire l'accès aux noeuds. Oublier cette clôture laisse l'API Talos de chaque noeud publiée.

Déclarer ce qui survit plutôt que ce qui part#

Chaque environnement a maintenant un petit fichier qui liste, par racine OpenTofu, les ressources qu'une mise en veille ne doit pas détruire. Chaque plan de destruction exclut tous ces survivants déclarés, si bien que le sort par défaut de toute nouveauté est d'être détruite. Une ressource ajoutée le mois prochain part à la prochaine mise en veille, sauf si quelqu'un décide, par écrit, qu'elle doit rester.

Les survivants sont ce qu'une reconstruction ne peut pas recréer. Le bundle de secrets Talos en est l'exemple le plus net : un nouveau bundle, c'est un autre cluster, et le perdre oblige à réinitialiser chaque noeud à la main. À côté se trouvent les secrets déposés à la main, le mot de passe généré de la base (pour qu'une base reconstruite reprenne celui que sa copie stockée porte déjà), le namespace du registre, ainsi que l'adresse de sortie et l'enregistrement du challenge DNS que les enregistrements de la zone désignent. En dessous de tout ça se trouve un socle qu'aucun plan ne peut atteindre : le projet, ses identifiants, les buckets d'état et d'images, l'adresse réservée de l'edge et l'image des noeuds.

Avant de confier l'option d'exclusion à un vrai environnement, j'ai mesuré ce qu'elle fait en mode destruction, dans une racine jetable, avec la version exacte d'OpenTofu utilisée. Les ressources exclues et tout ce dont elles dépendent sont conservées, tandis que ce qui dépend d'elles est quand même détruit : c'est précisément ce qui permet au mot de passe de la base de survivre alors que la base qui le lit disparaît. Une ressource conservée n'apparaît pas du tout dans le plan, pas même comme un no-op, donc un survivant ne peut atteindre le garde-fou que par une exclusion manquante. Et une fois qu'il ne reste que des survivants, le même plan annonce qu'il n'y a rien à faire, ce qui explique qu'une deuxième mise en veille ne fasse rien au lieu d'échouer.

Chaque plan passe par un coupe-circuit#

Les deux sens passent par un seul point d'entrée, qui refuse la production par son nom avant de lire quoi que ce soit d'autre. Il ne charge le fichier d'identifiants de l'opérateur que s'il n'est lisible que par son propriétaire, et seulement s'il ne définit aucune des variables qui laisseraient une valeur venue d'un autre shell atteindre une racine.

Rien n'est appliqué en auto-approve. Chaque étape construit un plan enregistré, un petit garde-fou le lit en JSON et classe chaque changement, et seulement ensuite ce plan enregistré est appliqué : ce que le garde-fou a lu est donc ce qui s'exécute. Il sait ce que le moment interdit. À la remontée, il refuse toute suppression, tout remplacement et tout forget, et il refuse de créer un nouveau bundle de secrets Talos, en toutes circonstances. À la mise en veille, il refuse de toucher à ce qui est déclaré survivant ou fait partie du socle. Il affiche des actions et des adresses, jamais une valeur, parce que le JSON d'un plan contient les valeurs sensibles en clair.

Les étapes tournent sous errexit, donc une commande en échec arrête l'exécution au lieu de passer grâce à la dernière ligne de l'étape. Quelle que soit la façon dont une exécution se termine, erreur, interruption ou déconnexion, la sortie referme l'accès aux noeuds par le même garde-fou, et si elle n'y arrive pas, sa dernière ligne dit comment le fermer à la main. Le code de sortie distingue les deux cas, pour qu'un arrêt propre et un arrêt avec l'accès ouvert ne se ressemblent jamais.

Refuser tôt, et tout nommer#

Une remontée commence par trois vérifications, avant tout plan. La première contrôle le socle : les deux buckets, l'image des noeuds, le coffre à secrets, l'adresse de l'edge et les enregistrements DNS qui y répondent, ainsi qu'un identifiant qui ne peut rien atteindre de ce que l'amorçage lui a refusé. Deux de ces contrôles réussissent en étant refusés. Une clé capable de lister les applications IAM pourrait s'accorder n'importe quoi, donc un refus est la réponse que j'attends.

La deuxième compare les identifiants que portent les racines avec l'environnement réel et avec la fiche de l'environnement, parce que ces identifiants ont été recopiés à la main et qu'un identifiant d'image périmé désigne toujours une image réelle : un plan construit dessus réussit contre la mauvaise. La troisième prouve que chaque secret déclaré par une racine est soit déposé par la racine elle-même, soit nommé par une étape d'un runbook.

Chaque contrôle affiche une ligne de réussite ou d'échec par élément et continue après un échec, pour que la personne qui répare voie tous les problèmes en une seule exécution plutôt qu'un par tentative.

Ce qui a bloqué une mise en veille#

Deux choses extérieures à mon code ont décidé de la manière dont la mise en veille devait être écrite.

Le provider Terraform du fournisseur cloud, dans les deux versions précédant celle que j'épingle aujourd'hui, ne savait pas du tout détruire un serveur rattaché à un réseau privé. La suppression de l'interface privée répondait par un 412 depuis l'intérieur de la suppression du serveur, là où aucun ciblage ne pouvait changer l'ordre, et l'erreur ne nommait aucune ressource. Arrêter les serveurs n'y changeait rien. La correction est arrivée en amont dans la version suivante, les racines y sont passées, et la mise en veille ne porte aucun contournement.

L'autre est un réseau de passerelle qui peut rester bloqué dans un état de détachement, sans cause ni remède documentés. La racine cloud est donc détruite en deux plans : le premier garde le réseau de passerelle et ce sur quoi il repose, le second le supprime hors bande une fois le garde-fou passé, puis attend cinq minutes au plus. Le provider considère un réseau de passerelle déjà disparu comme supprimé, ce qui permet au plan enregistré de s'appliquer quand même. En cas de dépassement, l'exécution nomme les identifiants et affiche ce qui retient encore le réseau privé, plutôt que de deviner.

Une mise en veille supprime ensuite ce qui survit à toutes les racines, les snapshots et backups de base dont l'instance a disparu et les volumes laissés par un PersistentVolumeClaim, puis se termine par un inventaire de tout le projet. Tout ce que ni un survivant ni le socle n'explique fait échouer l'exécution. C'est cet inventaire qui remplace la lecture de la console, pour qu'une ressource qu'une racine a cessé de suivre apparaisse dans un terminal plutôt que sur une facture.

Remonter sans personne#

La remontée décide elle-même de ce qu'elle a à faire. Quand le plan au repos du cluster est vide et qu'aucun noeud ne porte de configuration en attente, elle écrit les configurations clientes, attend que chaque noeud soit prêt et n'ouvre jamais l'accès aux noeuds : une deuxième exécution contre un environnement qui tourne n'applique rien. Sinon, elle ouvre l'accès, sonde chaque noeud, et ne demande une application automatique qu'aux noeuds encore en mode maintenance, parce que les applications tournent en parallèle et que la même demande sur un control plane en marche redémarrerait les trois noeuds d'un coup. Les noeuds en attente sont redémarrés un par un, avec un contrôle de santé après chacun.

Un comportement d'OpenTofu a coûté une remontée réelle avant que le script ne le gère. Appliquer un plan enregistré relit les fichiers de variables commités de la racine et refuse toute valeur qui diffère de celle que le plan a enregistrée, mais ne compare jamais une valeur par défaut. Un plan construit avec une surcharge en ligne de commande face à un fichier commité s'arrête donc à l'application, alors que la même surcharge face à une valeur par défaut passe sans problème. Le script répète maintenant exactement les surcharges avec lesquelles il a planifié, au lieu de compter sur le fichier de plan pour les porter.

Chaque attente est bornée et dit ce qu'elle a vu en dernier quand elle abandonne. L'exécution se termine en prouvant que l'edge répond sur l'hôte de l'environnement, en appliquant la racine de la plateforme et en attendant que l'application racine d'Argo CD soit synchronisée et en bonne santé.

Ce que disent les chiffres, et ce qu'ils ne disent pas#

Les durées viennent des journaux d'exécution. La mise en veille a pris 87 secondes, surtout pour les deux destructions côté cloud. La remontée a pris 677 secondes, surtout pour la racine cloud et l'application du cluster, un peu plus de trois minutes chacune. Cette remontée reconstruisait un environnement mis en veille quelques minutes plus tôt, donc la paire mesure la machinerie et non les données : pas de restauration d'etcd, et aucune charge de clients à ramener. Un vrai objectif de reprise demande cet exercice-là, et je n'en annoncerai pas avant qu'il ait eu lieu.