
Un an de Talos en production
Retour d'expérience après douze mois de Talos Linux en production, en homelab et sur des clusters mononœuds jetables : provisionnement uniquement en Terraform, mises à jour sans coupure, ce qu'apporte réellement un rootfs en lecture seule, et les deux points qui coincent encore.
Il y a un an, le cluster tournait sur des nœuds Linux classiques : une distribution, un outil de gestion de configuration, un bastion SSH, une procédure d’astreinte pleine de commandes à taper sur un nœud à trois heures du matin. Aujourd’hui il tourne sur Talos, et le résumé honnête de l’année est que le système d’exploitation a cessé d’être un sujet.
Cette phrase est facile à écrire et difficile à mériter, donc voici le détail : à quoi ressemble réellement le cluster, ce qu’ont donné un an de mises à jour et de tests de panne, et les deux choses que j’aimerais encore voir corrigées.
Le cluster de production est le cas exigeant et il occupe l’essentiel de la place, mais il ne résume pas l’année. Le même Talos tourne dans mon homelab, et sur les clusters mononœuds que je monte pour évaluer un logiciel et que je supprime le soir même. Que ces trois contextes soient un seul système d’exploitation, un seul outil et un seul format de configuration s’est révélé être un enseignement en soi, donc il a droit à sa propre section plus bas.
Sommaire
Le cluster
Baremetal, trois sites, un seul cluster Kubernetes.
site A site B
┌────────────────────────────────┐ ┌────────────────────────────────┐
│ cp-01 control plane │ │ cp-02 control plane │
│ │ │ │
│ worker-01 .. worker-05 │ │ worker-06 .. worker-10 │
│ (OSD Ceph + charges de travail)│ │ (OSD Ceph + charges de travail)│
└───────────────┬────────────────┘ └───────────────┬────────────────┘
│ │
│ L3 entre les sites │
└───────────────┬───────────────────────┘
│
┌────────────┴─────────────┐
│ site C │
│ cp-03 control plane │
│ aucun worker, aucune │
│ donnée : troisième │
│ homme pour le quorum │
└──────────────────────────┘Le site C ne porte rien d’autre qu’un nœud de control plane. Il existe pour que la perte du site A ou du site B laisse deux membres etcd sur trois, donc une majorité, donc un API server qui répond toujours. C’est la brique la moins chère de toute la conception, et celle qui décide si une panne de site est un incident ou un non-événement.
Chaque worker est un serveur bi-socket à treize disques, et la répartition compte plus que le nombre :
worker-0X
┌──────────────────────────────────────────────────────────────────────┐
│ 2 x SSD, RAID 1 matériel ─▶ disque système Talos, rootfs read-only│
│ 1 x NVMe ─▶ user volume Talos, xfs, PV local-path │
│ 1 x NVMe ─▶ OSD Ceph, device class `nvme` │
│ 1 x NVMe ─▶ métadonnées Ceph (RocksDB/WAL) │
│ 4 x SAS 10k ─▶ OSD Ceph, device class `sas-fast` │
│ 4 x SAS 7.2k ─▶ OSD Ceph, device class `sas` │
└──────────────────────────────────────────────────────────────────────┘
13 disques, tous sélectionnés par leur entrée /dev/disk/by-pathTrois choses à retenir de cette répartition, et seule la première parle vraiment de disques.
Chaque disque est déclaré par son bus path, jamais par /dev/sdX ni
/dev/nvme0n1. L’ordre d’énumération du noyau n’est pas un contrat : il peut
changer après un redémarrage, une mise à jour de firmware ou un remplacement de
fond de panier. Sur un nœud dont toute l’identité est un fichier de
configuration, ce n’est pas un détail cosmétique : c’est la différence entre
« installer sur le volume RAID 1 » et « installer sur un OSD ». Le disque
système est sélectionné par busPath dans la configuration Talos, et les
disques Ceph sont remis à Rook via un devicePathFilter sur les mêmes entrées
/dev/disk/by-path. Prenez les dix minutes nécessaires pour relever les bus
paths. C’est la garantie la moins chère de toute l’installation.
Le disque système est un volume RAID 1 matériel, volontairement. Talos n’a ni gestionnaire de paquets ni shell : la panne d’un disque système n’est pas quelque chose qu’on répare en se connectant. Le contrôleur la masque, le nœud continue de tourner, et le disque est remplacé en heures ouvrées plutôt qu’en incident. De toute façon rien de précieux n’y vit : la configuration machine est dans Git.
Le disque local est un user volume Talos, formaté en xfs par Talos lui-même, et consommé par local-path-provisioner. C’est une réponse volontairement provisoire à « certaines charges veulent un PV local rapide et sans réplique », et elle sera revue. Elle mérite d’être signalée parce que c’est le seul stockage que Talos provisionne directement : tout ce qui touche à Ceph n’est déclaré nulle part dans la configuration Talos, reste brut, et est réclamé par l’opérateur de stockage. Déclarer un disque d’OSD des deux côtés, c’est se retrouver avec une table de partitions sur laquelle deux systèmes ne sont pas d’accord.
Côté Ceph, une décision de conception mérite d’être signalée puis laissée de côté : les métadonnées (RocksDB et WAL) sont sur un NVMe dédié pour garder les OSD SAS rapides, ce qui fait de ce seul périphérique un domaine de panne partagé pour tous les OSD qui en dépendent. Le gain de performance est réel et le sujet est sur la liste des choses à refaire. C’est aussi une question Ceph plutôt qu’une question Talos, donc elle s’arrête ici.
Provisionnement : Terraform, et c’est toute l’histoire
Le cluster, c’est un état Terraform et un répertoire de templates Jinja2. Pas d’Omni, pas d’orchestrateur PXE avec sa propre base, aucun agent de gestion de configuration sur les nœuds.
resource "talos_machine_secrets" "this" {}
data "talos_machine_configuration" "this" {
for_each = var.nodes
cluster_name = var.cluster_name
cluster_endpoint = "https://${var.api_vip}:6443"
machine_type = each.value.role # controlplane | worker
machine_secrets = talos_machine_secrets.this.machine_secrets
talos_version = var.talos_version
# un patch rendu par domaine, appliqué dans un ordre fixe
config_patches = local.patches[each.key]
}
resource "talos_machine_configuration_apply" "this" {
for_each = var.nodes
client_configuration = talos_machine_secrets.this.client_configuration
machine_configuration_input = data.talos_machine_configuration.this[each.key].machine_configuration
node = each.value.ip
}local.patches est l’endroit où atterrit l’étape de rendu : pour chaque nœud,
chaque template rendu avec les données de ce nœud, à savoir son rôle, sa zone,
ses adresses et ses bus paths. Les templates sont découpés par domaine, jamais
par machine :
patches/
├── network.yaml.j2 # adresses, VIP, bonding
├── dns.yaml.j2
├── ntp.yaml.j2
├── proxy.yaml.j2
├── registry.yaml.j2 # miroirs, par site
├── disks.yaml.j2 # disque système, user volumes
├── kubelet.yaml.j2
└── extensions.yaml.j2Ce découpage est la partie que je garderais sur n’importe quel cluster suivant.
Un fichier par domaine, c’est « changer les serveurs NTP » qui devient un diff
d’un seul fichier, relisible par quelqu’un qui ne connaît rien au reste de la
configuration. Et quand Talos expose un vrai document dédié à un domaine, le
patch est ce document plutôt qu’un fragment v1alpha1 : UserVolumeConfig pour
le volume local, ExtensionServiceConfig pour les réglages d’extension,
NetworkRuleConfig pour le pare-feu en entrée. Ces documents sont validés pour
eux-mêmes, donc une erreur apparaît au moment de l’application au lieu d’être
fusionnée silencieusement dans une map imbriquée.
Les différences entre rôles et entre sites vivent à l’intérieur des templates, sous forme de conditions, pas de fichiers dupliqués. Le patch des disques est l’exemple le plus clair, puisque c’est aussi là que la répartition ci-dessus devient lisible par la machine :
# patches/disks.yaml.j2
machine:
install:
# sélectionné par son bus path, jamais par /dev/sda, qui n'est vrai
# que jusqu'au prochain redémarrage
diskSelector:
busPath: '{{ host.system_disk_bus_path }}'
{% if host.role == 'worker' %}
kubelet:
extraMounts:
- destination: /var/mnt/local-path
type: bind
source: /var/mnt/local-path
options: ['bind', 'rshared', 'rw']
---
apiVersion: v1alpha1
kind: UserVolumeConfig
name: local-path
provisioning:
diskSelector:
match: disk.bus_path == '{{ host.local_disk_bus_path }}'
filesystem:
type: xfs
{% endif %}Talos formate et monte ce volume lui-même, sous /var/mnt/local-path. L’entrée
extraMounts est ce qui permet à local-path-provisioner de le remettre à un
pod. Un nœud de control plane ne rend rien de ce bloc : il a deux disques et
rien à provisionner.
Tout le reste, les dix disques Ceph compris, est délibérément absent.
L’axe du site fonctionne pareil, et le site C est le cas intéressant justement parce que ce n’est pas un site de données :
# patches/registry.yaml.j2
machine:
registries:
mirrors:
docker.io:
endpoints:
- https://mirror.{{ host.zone }}.example.internal
{% if host.zone == 'dc3' %}
# pas de miroir local : site de quorum, aucune charge à servir
- https://mirror.dc1.example.internal
{% endif %}Deux axes, role et zone, et s’y tenir est toute la discipline. Six
configurations complètes, une par rôle et par site, auraient divergé en un
trimestre. Deux axes de conditions dans un template par domaine, non, parce que
la partie commune est physiquement commune plutôt que copiée et maintenue
alignée à la force du poignet.
Le coût est réel : on ne peut plus lire la configuration d’un nœud sur le
disque, seulement le template qui la produit. La réponse, c’est
talosctl apply-config --dry-run, qui affiche le diff par rapport à ce que le
nœud exécute actuellement. Rendre, comparer, puis appliquer. C’est cette
habitude qui garde une configuration templatée honnête.
Ce que ça apporte n’est pas « l’infrastructure as code » comme slogan. C’est une propriété précise et vérifiable : la configuration machine est le nœud tout entier. Il n’y a pas de dérive à réconcilier parce qu’il n’existe pas de second endroit où faire un changement. Personne n’a jamais réparé un nœud à la main ici, parce que c’est impossible.
Omni est un bon produit et je comprends pourquoi il existe : il règle d’un coup la découverte, l’enrôlement et les accès, pour des parcs qui ne sont pas décrits dans un état Terraform. Le nôtre l’est. Ajouter un control plane SaaS, ou l’auto-héberger, pour gérer treize machines déjà décrites dans Git, c’était acheter une deuxième source de vérité au prix de la première.
Mises à jour : douze mois, zéro coupure
Mise à jour des nœuds :
talosctl -n worker-07 upgrade \
--image factory.talos.dev/installer/<schematic>:v1.x.yMise à jour de Kubernetes :
talosctl -n cp-01 upgrade-k8s --to 1.3x.yLa seconde commande est celle qui a changé le rapport de l’équipe aux versions mineures de Kubernetes. Elle parcourt les composants du control plane puis les kubelets dans l’ordre, un nœud à la fois, et c’est une commande unique plutôt qu’une procédure.
Sur l’année, les mises à jour de nœuds et de Kubernetes n’ont produit aucune coupure et aucune fenêtre dégradée visible par un utilisateur. La sensation opérationnelle est celle d’EKS ou d’AKS : on choisit une version, on déclenche, on regarde le rolling. La différence est que les nœuds sont les nôtres, dans nos baies, et que le control plane n’est pas une boîte noire à laquelle on ouvre un ticket.
Deux raisons à cela, et aucune ne relève de la chance :
- La mise à jour est un échange d’image A/B, pas une transaction de paquets. Un nœud revient soit sur la nouvelle version, soit sur la précédente. Il n’existe pas d’état à moitié mis à jour à déboguer, qui est précisément l’état qui transforme une montée de version de distribution en incident.
--stageexiste pour les cas où une charge de travail ne se draine pas proprement. La mise à jour est écrite sur le disque et appliquée au redémarrage suivant, ce qui permet de choisir la fenêtre du reboot.
Le rootfs en lecture seule, et ce qu’il couvre réellement
Une élévation de privilèges locale, côté noyau ou côté espace utilisateur, a fait parler d’elle récemment. Le cluster n’était pas exposé, et il vaut la peine d’être précis sur le pourquoi, parce que « OS immuable » sert trop souvent de formule magique.
Talos supprime le chemin d’exploitation, pas la faille :
- il n’y a ni shell ni démon SSH, donc aucune session interactive sur le nœud depuis laquelle escalader ;
- il n’y a ni gestionnaire de paquets ni
/usrinscriptible, donc un binaire déposé n’a nulle part où persister ; - le système de fichiers racine est en lecture seule et vérifié, donc modifier un binaire système n’est pas une question de permissions mais de refus d’écriture ;
- la seule interface est l’API machine, en TLS mutuel, avec un rôle porté par le certificat client.
Ce qu’il ne fait pas, c’est corriger le noyau. Une exécution de code à distance joignable depuis un service réseau, ou une évasion de conteneur, reste quelque chose qu’on corrige en passant sur une version de Talos qui embarque le noyau patché. La différence, c’est que cette mise à jour est la commande de la section précédente, appliquée un mardi, au lieu d’une montée de version de distribution coordonnée sur treize machines.
Le cadrage honnête : Talos ne rend pas immunisé, il vide le rayon d’explosion de toute une classe de vulnérabilités locales, et il rend le correctif routinier.
Tests de panne, tous passés
Tout ce qui suit a été testé délibérément, sur le cluster de production, pendant des fenêtres de maintenance :
| Test | Résultat |
|---|---|
| Perte brutale d’un worker | Pods replanifiés, Ceph reconstruit, nœud réintégré au boot |
| Redémarrage d’un nœud control plane | Membre etcd réintégré, aucune interruption d’API |
| Panne totale du site A | Quorum tenu par cp-02 et cp-03, charges déplacées sur le site B |
| Coupure du lien entre sites | Côté minoritaire isolé, côté majoritaire toujours en service |
| Reboot glissant de tous les workers | Aucune indisponibilité applicative, aucune étape manuelle |
Rien dans cette liste n’a demandé à un humain de taper quoi que ce soit sur un nœud. Dans tous les cas, la reprise a été : le courant revient, le nœud démarre, il récupère sa configuration, il réintègre. Un nœud Talos n’a aucun état digne d’être préservé entre deux redémarrages, donc « est-il revenu correctement » cesse d’être une question.
Un point que les tests ont fait ressortir, et ce n’est pas un problème Talos : le quorum etcd n’est pas le seul quorum. Le stockage réparti sur deux sites de données a le sien, et il se conçoit en même temps que la topologie du control plane, pas après.
Ce que j’aimerais voir corrigé : les accès à talosctl
C’est mon seul vrai reproche après un an, et ce qui le rend plus criant, c’est que le même cluster résout déjà exactement ce problème à l’étage au-dessus.
L’API server Kubernetes s’authentifie auprès d’Entra ID en OIDC. L’accès est
une appartenance à un groupe : quelqu’un entre dans la rotation d’astreinte,
atterrit dans le bon groupe, son kubectl fonctionne, et cesse de fonctionner
le jour du départ. Rien n’est distribué à la main, rien n’est révoqué certificat
par certificat.
L’API machine de Talos est l’étage en dessous, et elle fonctionne autrement.
L’accès est un certificat client portant un rôle (os:admin, os:operator,
os:reader). Le modèle de permissions est sain, et c’est un vrai progrès par
rapport à « toute personne avec une clé SSH est root ». Ce qui lui manque, c’est
cette même passerelle vers le fournisseur d’identité. Un seul cluster se
retrouve donc avec deux histoires d’accès : Kubernetes en SSO, et les machines
en dessous avec des certificats qui circulent.
Sur des nœuds Linux classiques, c’est aussi un problème résolu : un groupe LDAP,
une règle sudo, une autorité de certification SSH, et la personne entrée dans
la rotation la semaine dernière obtient l’accès en rejoignant un groupe. Il
n’existe pas d’équivalent dans Talos open source. Omni fournit du SSO, mais cela
revient à adopter Omni pour une fonctionnalité de contrôle d’accès, ce qui
ramène la deuxième source de vérité évoquée plus haut.
Ce que nous faisons à la place fonctionne, mais demande plus de mécanique qu’il ne devrait : un certificat d’opérateur à durée de vie courte, émis à la demande plutôt que distribué.
talosctl config new /tmp/oncall.talosconfig \
--roles os:reader \
--crt-ttl 8hCe n’est pas une appartenance à un groupe, ce n’est pas audité de façon centrale, et quelqu’un doit lancer la commande. Le manque n’est pas le modèle de permissions, c’est l’enrôlement.
Quelqu’un a déjà construit la pièce manquante :
talosctl-oidc, de Quentin Joly,
présenté dans cet article. La
forme est exactement celle que le problème appelle. Un serveur détient la clé de
la CA Talos et authentifie l’utilisateur auprès du fournisseur d’identité, les
claims OIDC sont projetés sur des rôles Talos (un groupe platform-admins vers
os:admin, un groupe developers vers os:reader), et le client échange un ID
token contre un certificat éphémère, cinq minutes par défaut, écrit directement
dans le talosconfig. Il automatise la commande ci-dessus, puis fait ce qu’elle
ne peut pas faire : déduire le rôle d’un groupe plutôt que de la personne qui a
tapé la commande.
Deux points à peser avant de le mettre sous une astreinte. Le projet est jeune, et il détient la clé privée de la CA, ce qui en fait un composant à protéger comme une autorité de certification et non comme un binaire utilitaire. Ni l’un ni l’autre n’est une raison de l’ignorer. C’est la bonne réponse au bon problème, et je préférerais voir cette forme arriver en amont plutôt que continuer à maintenir le contournement.
Les autres aspérités : le matériel, et pas vraiment Talos
Deux domaines ont pris du temps cette année, et dans les deux cas je pense que Talos est le messager plutôt que la cause.
Effacer un disque. Réinitialiser un OSD ou un cluster de stockage entier est
pénible, parce que le réflexe habituel est un shell sur le nœud et un
sgdisk --zap-all, qui n’existe pas ici. Ce qui fonctionne, c’est un Job
privilégié avec le périphérique monté, ou talosctl wipe disk sur les versions
récentes de Talos. Les deux vont très bien une fois écrits. La friction, c’est
que presque toutes les procédures publiées en ligne supposent un nœud sur lequel
on peut se connecter : il faut les traduire avant de pouvoir s’en servir.
Les GPU. Les pilotes sur Talos sont des extensions système intégrées à l’image de démarrage via l’Image Factory, ce qui épingle la version du pilote à la version de Talos par un schematic. Mettre Talos à jour implique de reconstruire le schematic ; le module noyau, le container toolkit et le device plugin doivent tous être d’accord. Quand le constructeur publie une extension pour la carte, c’est propre et reproductible, meilleur qu’un pilote installé par un DaemonSet à l’exécution. Quand il traîne, ou quand la carte est exotique, on attend. C’est un problème de constructeur et d’écosystème, qui se manifeste à la frontière de Talos.
Aucun des deux ne m’a fait reconsidérer le choix. Les deux méritent d’être connus avant de s’engager sur un cluster à forte densité de stockage ou de GPU avec un OS immuable.
La même chose en homelab et sur des clusters jetables
Une bonne partie de l’usage de Talos cette année n’était ni de la production ni treize machines. C’était le homelab, et des clusters mononœuds montés pour essayer un logiciel et démontés une fois la question tranchée.
Un cluster mononœud, c’est la même configuration avec une ligne changée :
cluster:
allowSchedulingOnControlPlanes: trueMêmes templates, même talosctl, même commande de mise à jour, mêmes patches
découpés par domaine, avec un axe role qui n’a qu’une valeur. Rien ne change
dans la façon de travailler à un seul nœud, et c’est justement le point : ce
qu’on apprend sur un cluster jetable se transpose, parce que ce n’est pas un
autre système. Une distribution installée à la main sur une VM de test finit
toujours par diverger de ce que fait tourner la production, et chaque conclusion
qu’on en tire porte un astérisque.
L’autre moitié de la valeur, c’est la propreté de la disparition. Aucun état à
nettoyer, aucun hôte laissé à moitié configuré : talosctl reset ramène la
machine à rien, ou la VM est supprimée et rien de ce qu’elle contenait
n’importait. Évaluer trois opérateurs de stockage en une semaine cesse d’être un
exercice de désinstallation des deux précédents.
Ce qu’un seul nœud ne dit pas mérite d’être énoncé clairement. Le comportement du quorum, les domaines de panne et la façon dont une mise à jour glissante interagit avec les pod disruption budgets sont tous des propriétés du fait d’avoir plusieurs machines. Le PoC mononœud valide le logiciel. Il ne valide pas le cluster.
Ma conclusion
Talos était le bon choix, et une raison pèse plus lourd que toutes les autres.
Un nœud n’a plus de passé. Rien n’a été installé à la main pendant une session de débogage puis oublié. Personne n’a posé en 2024 un sysctl que plus personne ne sait expliquer. Le premier nœud construit et le dernier ajouté font tourner la même chose, parce que les deux sont le produit du même fichier de configuration.
Tout le reste de cette note en découle. Les mises à jour ne sont plus un événement. Les tests de panne passent. Un incident ne commence jamais par « tu peux me connecter sur le nœud ? ».
L’effort est le même sur du baremetal et en VM, et il reste faible. Il n’y a pas de dette technique à porter, parce qu’elle n’a nulle part où s’accumuler.
Les deux reproches ci-dessus tiennent toujours. L’accès à talosctl a besoin
d’un vrai fournisseur d’identité, et certains matériels demandent encore de la
patience. Aucun des deux n’est une raison de revenir en arrière.

