Tous les articles

Tous les articles

Monter un parc Kubernetes autohébergé sur Talos et Scaleway

Pourquoi la plateforme fait tourner son propre control plane Kubernetes sur Talos et Scaleway, et les pièges du premier cluster monté depuis OpenTofu.

Le 10 septembre, le cluster de staging a démarré : trois noeuds de control plane Talos et deux noeuds système sur des Instances Scaleway, tous déclarés dans OpenTofu, et un cluster capable de planifier un pod dès qu'etcd a été amorcé. Jusque-là, le parc n'existait que dans un runbook et dans ce que j'avais cliqué dans la console.

Pourquoi pas l'offre managée#

Scaleway vend un Kubernetes managé, et la question évidente est de savoir pourquoi je ne l'ai pas pris. La plateforme fera tourner des images fournies par ses clients : un namespace n'est donc pas une frontière de sécurité, et c'est le noyau qui doit en être une. Ça veut dire un runtime isolé (gVisor, et Kata pour ce que gVisor ne sait pas héberger) sur des noeuds partagés. Les pools de noeuds managés n'acceptent que les runtimes de conteneurs standards, et leurs images de noeud ne sont pas les miennes à étendre. Le control plane managé ne me laisse pas non plus de politique d'audit à moi, aucune prise sur les flags de l'API server ni sur le chiffrement au repos, et un etcd plafonné, alors que tout l'historique Helm de chaque client vivrait dans cet etcd.

Le control plane est donc le mien : trois noeuds, pour le quorum plutôt que pour le coût. Le staging reproduit la forme de la production, à la plus petite taille qui la prouve encore, parce qu'un quorum qui n'existe qu'en production est un quorum dont personne n'a répété la perte. Talos sur chaque noeud, c'est un seul système d'exploitation partout, donc une seule chaîne d'images et un seul mécanisme de mise à jour. Le design, chiffré avant que rien de tout ceci ne soit construit, revenait à environ 218 euros par mois avant le premier client.

Trois couches, à cause du moment où les providers sont configurés#

L'organisation d'OpenTofu est venue en premier, la veille, et elle ne crée rien. Deux de ses choix sont imposés plutôt que stylistiques, et tous deux coûtent cher à découvrir une fois qu'un état existe.

Chaque environnement est un répertoire plutôt qu'un workspace. tofu workspace select garde le dernier choix, sans rien dire, si bien qu'une organisation en workspaces permet d'atteindre la production en oubliant une commande plutôt qu'en la tapant.

Chaque environnement est aussi découpé en racines : une pour les ressources cloud, une pour le cluster construit dessus. La configuration des providers est évaluée au moment du plan, donc une racine unique qui crée un cluster puis configure un provider Kubernetes à partir de ses propres sorties ne peut pas planifier une seule fois. La racine du cluster lit plutôt l'état de la racine cloud.

OpenTofu plutôt que Terraform était imposé aussi. Sur un stockage compatible S3, l'objet de verrou du backend ignorait un flag de checksum : les écritures d'état réussissaient alors que la prise du verrou échouait. OpenTofu l'a corrigé, et le rapport équivalent côté Terraform a été fermé comme doublon avec un contournement. OpenTofu chiffre aussi l'état, ce que la CLI Terraform ne fait pas du tout. Le bloc de chiffrement ne porte aucune passphrase, si bien que le formatage, le lint et la validation passent tous sur un checkout sans le moindre secret, et que la CI n'a aucun identifiant pour appliquer quoi que ce soit. En ajouter un serait une modification visible d'un workflow, pas un changement de comportement discret.

Le linter a été rentable dès le premier jour : il a trouvé la racine du cluster qui épinglait deux providers que rien n'utilisait, ce qui coûtait 158 Mo de téléchargements à une exécution à froid au lieu de 43.

La couche cloud#

Un réseau privé avec une passerelle partagée, une instance PostgreSQL joignable uniquement de l'intérieur, le bucket de l'application, un namespace de registre, des conteneurs de secrets, et les cinq machines. Tailles et nombres sont des variables : un deuxième environnement est un fichier de variables, pas un fork.

L'API Scaleway a renversé trois hypothèses. L'adresse que j'avais réservée pour la périphérie appartenait à une famille qu'aucun load balancer ne peut lier, et les deux familles ne se convertissent pas. L'identifiant qui construit le parc ne détient aucune permission IAM, ce qui est de toute façon la bonne réponse : écrire de l'IAM demande un droit dont le titulaire peut s'accorder n'importe quoi, donc ces applications restent manuelles. Et le SDK préfère ses propres variables d'environnement aux arguments que le provider lui passe : avec les variables habituelles exportées, c'était le shell, et pas le fichier de variables, qui décidait dans quel projet une racine construisait. Elles ont été renommées.

Un détail vaut d'être lu deux fois. Le mot de passe de la base part dans Secret Manager comme attribut en écriture seule, ce qui le garde hors de l'état et des fichiers de plan. Une valeur en écriture seule n'est renvoyée que quand son compteur de version change, et elle se relit comme vide. Avec un compteur figé sur une constante, un nouveau mot de passe aurait atteint la base pendant que Secret Manager gardait l'ancien, sans que rien n'échoue. Une seule entrée génère désormais le mot de passe et versionne le secret, si bien qu'une rotation fait bouger les deux moitiés dans le même apply.

La couche cluster#

La racine du cluster écrit la configuration machine des deux rôles, l'applique à des machines qui n'ont encore rien rejoint, amorce etcd, et embarque Cilium, le cloud controller manager et le driver de stockage sous forme de manifestes, si bien que le cluster démarre déjà capable de faire tourner une charge.

Les noeuds n'ont pas d'adresse publique. L'API Kubernetes se trouve derrière un load balancer privé plutôt qu'une adresse virtuelle Talos, qui est la réponse la moins chère et celle que j'avais écrite en premier. Scaleway demande un enregistrement de MAC avant de transporter une adresse qu'il n'a pas allouée, limite le broadcast par lequel un basculement s'annonce, et ne dit rien sur le fait de transmettre ou non des trames pour une adresse qu'il n'a jamais attribuée. Deux inconnues pour une seule adresse, c'était l'argument contre.

Atteindre l'API Talos d'un noeud passe par un interrupteur coupé au repos. Un noeud en mode maintenance, ce qu'est précisément un noeud neuf ou effacé, ne présente aucun certificat et accepte une configuration de quiconque la lui envoie : cette porte ne s'ouvre donc que pendant que je m'en sers.

Le bundle de secrets machine, l'identité du cluster, est importé plutôt que généré, et OpenTofu refuse de le détruire. Un deuxième bundle n'est pas une réparation : c'est un autre cluster, auquel les noeuds en marche ne parleront pas, et se tromper veut dire réinitialiser les cinq machines.

Les après-midi#

Quatre réglages encodent chacun un piège qui a pris un après-midi à trouver plutôt qu'à corriger.

Le volume éphémère est choisi par sa taille plutôt que par son chemin de périphérique, parce que le chemin donné par le guide Scaleway est juste jusqu'à ce que les volumes soient rattachés dans un autre ordre, et désigne alors le disque système. L'adresse de la passerelle doit figurer dans les certificats des machines, et c'est l'oubli qui coûte cher : le provider sonde une machine en mode maintenance sans rien vérifier, donc le premier apply passe et le refus arrive plus tard, d'une machine qu'on ne peut plus atteindre pour la corriger. L'image d'installation est épinglée sur celle qui porte les extensions d'isolation, parce que l'installeur standard retirerait gVisor et Kata de chaque noeud lors d'une mise à jour, en annonçant un succès. Et le kubelet est limité au sous-réseau privé, sans quoi il peut enregistrer une adresse IPv6 à côté de l'IPv4, et chaque vérification qui cherche un noeud par son IPv4 signale un noeud introuvable.

Le mode d'application est une variable, parce que le réglage sûr dépend de l'opération. Le plus agressif, sur un cluster en marche, redémarre tous les noeuds de control plane en même temps, puisque rien n'ordonne les applications, et emporte le quorum etcd avec eux.

Deux choses ne sont apparues qu'une fois les charts rendus dans la configuration machine. Le chart de Hubble fabrique une autorité de certification à l'intérieur du template, si bien que deux rendus de la même version diffèrent et que la configuration ne se stabilise jamais : il est désactivé. Et Talos est livré avec la découverte activée vers un service hébergé, si bien qu'un cluster sans adresse publique annoncerait quand même sa composition à un tiers. Désactivée aussi.

La suite#

Un noeud Talos demande une image sur mesure par environnement et par mise à jour, et deux des six commandes manuelles qui en construisaient une échouaient tard : l'import dans le stockage refusait une image brute seulement après un envoi de 4,45 Go. Le 27 septembre, elles sont devenues un seul script, qui lit l'en-tête de l'image avant d'envoyer quoi que ce soit, réutilise ce qu'une exécution précédente a laissé, et refuse une image qu'une attente a laissée en erreur, parce que les attentes de la CLI se terminent avec succès sur un échec comme sur une réussite.

Tout ce qui vient après le cluster, des secrets aux certificats jusqu'à l'application elle-même, est livré depuis git par Argo CD plutôt que par OpenTofu. C'est l'objet d'un autre article.