← Tous les articles
Juin 2026 · 16 min · Kubernetes

De Kubeadm à Talos in-place : neuf clusters de prod, zéro downtime

Neuf clusters, deux cents nœuds, un cloud OpenStack privé, aucune fenêtre de maintenance. Voici la procédure que j'aurais aimé lire avant — l'ordre dans lequel les étapes doivent se faire, et les quatre choses qui cassent silencieusement si on se trompe.

Chez Inherent, nous avons neuf clusters sur notre infrastructure OpenStack, tous à l’origine déployés avec Ansible et Kubeadm.

Ce n’est pas un article de blog destiné à présenter Talos : je pense qu’il existe beaucoup d’articles sur Internet qui le font mieux que moi. Mais ce qui nous a poussés à passer à Talos, c’est l’instabilité d’Ansible et des processus de déploiement qui en découlent. Ce n’est pas vraiment lié à Ansible directement, mais plutôt à la façon dont les déploiements ont été faits chez Inherent. Ce qui devait être une collection de playbooks simples s’est alourdi au fil des années : une configuration par-ci, un template Jinja par-là, puis un argument spécifique pour conclure. Au fil du temps, certains éléments ne sont plus maintenus, des changements sont faits à la main, etc.

Le déploiement ou la mise à niveau d’un cluster était devenu une tâche lourde, et il était rare que le playbook tourne de bout en bout sans accroc.

Talos se présentait comme une bonne porte de sortie et nous permettait de gérer toute notre infrastructure as code, grâce aux providers OpenStack et Talos. Cependant, des services critiques étaient déployés sur nos clusters : il fallait que le changement se fasse de façon transparente. Et nous sommes tombés sur cette documentation : https://docs.siderolabs.com/talos/v1.13/advanced-guides/migrating-from-kubeadm

La migration live, zero-downtime, était donc possible ! La documentation se suffit à elle-même ; j’écris cet article pour partager les problèmes que nous avons eus et la façon dont nous les avons résolus, dans l’espoir que cela soit utile à quelqu’un d’autre.

Voici les notes de terrain. Dans l’ordre, avec les pièges là où ils nous ont réellement mordu.

La clé de voûte : les PKI Kubeadm

Un cluster Talos n’est pas défini par ses nœuds. Il est défini par son bundle de secrets — CA du cluster, CA front-proxy, paire de clés de service account, CA etcd et token de bootstrap. Si vous générez ce bundle depuis la PKI de votre cluster kubeadm existant, un nœud Talos qui démarre avec ne forme pas un nouveau cluster. Il rejoint celui que vous avez déjà : même CA, donc les kubeconfigs et les tokens de service account existants restent valides ; même CA etcd, donc un control plane Talos devient membre votant du cluster etcd déjà en place.

talosctl gen secrets \
  --kubernetes-bootstrap-token <token> \
  --from-kubernetes-pki ./pki

Ci-dessus, la commande pour générer la PKI à partir de celle de Kubeadm. Ce seul flag transforme une reconstruction en remplacement progressif. Tout le reste de ces notes consiste à faire survivre le cluster au moment où les deux mondes se chevauchent — parce que pendant quelques heures, vous avez des nœuds kubeadm et des nœuds Talos dans le même cluster, et chaque add-on doit fonctionner sur les deux.

C’est la véritable contrainte de conception, et elle mérite d’être posée comme règle avant les étapes :

Chaque changement de préparation doit être sans effet sur les nœuds kubeadm et correct sur les nœuds Talos. Tout ce qui n’est correct que pour Talos doit être verrouillé pour ne pas redémarrer quelque chose qui sert du trafic maintenant.

Avant de toucher à quoi que ce soit

Un accès qui fonctionne aux nœuds kubeadm. Vous allez passer du temps sur ces machines, et c’est la dernière fois : Talos n’a ni SSH ni shell.

Sortez les providers Kubernetes et Helm du circuit. Si, comme nous, vous gérez une partie de la configuration de vos clusters via Terraform et les providers Kubernetes/Helm, désactivez-les pour l’instant. Ces ressources seront réimportées ensuite.

Épinglez volontairement la version de Talos, parce qu’elle épingle etcd. Nous avons pris la 1.10.6 pour la version d’etcd embarquée dans son image (3.5.21). VÉRIFIEZ BIEN LES SAUTS DE VERSION D’ETCD.

Lisez votre ClusterConfiguration kubeadm comme une spécification, pas comme un historique. Tout doit être transcrit fidèlement dans la machine config Talos :

  • controlPlaneEndpoint — identique, sinon tous les certificats et kubeconfigs du parc cassent.
  • podSubnet et serviceSubnet — identiques, non négociables.
  • Chaque entrée apiServer.extraArgs. Nos clusters s’authentifient via Keycloak ; les cinq flags OIDC ci-dessous vivent dans la conf kubeadm, et s’ils n’arrivent pas dans la conf Talos, le SSO continue de fonctionner jusqu’au drain du dernier apiserver kubeadm — puis tout le monde perd l’accès d’un coup.
  • Le fait que les control planes soient schedulables, et tous les taints retirés à la main il y a des années.

Étape 1 — Ajouter localhost aux certSANs kubeadm

Talos fait passer le trafic API interne par KubePrism, un proxy local qui écoute sur localhost:7445 sur chaque nœud. Les composants Talos parlent donc à l’apiserver via la loopback — et votre certificat d’apiserver actuel, généré par kubeadm, n’a très probablement ni 127.0.0.1 ni localhost dans ses SANs.

À faire sur les trois control planes kubeadm, avant tout le reste. Commencez par le master qui a encore à coup sûr la conf kubeadm sur disque — si les autres ne l’ont pas, copiez-la depuis le premier plutôt que de la reconstituer.

# On garde le certificat actuel. C'est votre rollback.
mv /etc/kubernetes/pki/apiserver.{crt,key} ~

# Ajouter 127.0.0.1 et localhost dans apiServer.certSANs.
# Éditez une fois sur le premier master, puis copiez le fichier sur les deux autres.
vim /etc/kubernetes/unyc-manifests/config-kubeadm.yaml

# Régénérer le certificat de service depuis la conf mise à jour.
kubeadm init phase certs apiserver --config /etc/kubernetes/unyc-manifests/config-kubeadm.yaml

# Persister le changement dans la ConfigMap kubeadm-config du cluster.
kubeadm init phase upload-config kubeadm --config /etc/kubernetes/unyc-manifests/config-kubeadm.yaml

La configuration, avec les SANs en place :

apiVersion: kubeadm.k8s.io/v1beta3
kind: ClusterConfiguration
kubernetesVersion: stable
clusterName: "<cluster>-k8s"
imageRepository: "registry.k8s.io"
controlPlaneEndpoint: "k8s.<env>.example.internal:6443"
networking:
  podSubnet: 192.168.0.0/19
  serviceSubnet: 192.168.32.0/19
apiServer:
  certSANs: ['k8s.<env>.example.internal', '127.0.0.1', 'localhost']
  extraArgs:
    oidc-issuer-url: "https://sso.example.com/realms/<realm>"
    oidc-client-id: "kubernetes-sso-<cluster>"
    oidc-username-claim: "preferred_username"
    oidc-username-prefix: "oidc:"
    oidc-groups-claim: client_roles
    oidc-groups-prefix: "oidc:"

Le kubelet voit le nouveau fichier de certificat et redémarre le static pod de l’apiserver de lui-même. Vérifiez que le changement est bien pris en compte avant de continuer — openssl s_client -connect 127.0.0.1:6443 </dev/null | openssl x509 -noout -text | grep -A1 'Subject Alternative Name' — et faites les trois masters un par un, en attendant que chaque apiserver revienne sain. Cette étape est additive et sans danger sur un cluster vivant ; c’est aussi celle que l’on saute parce qu’elle a l’air cosmétique.

Étape 2 — Extraire la PKI et générer le bundle de secrets

Deux choses doivent sortir d’un control plane vivant : le répertoire PKI et un token de bootstrap qui n’expirera pas en pleine migration.

# Sur un nœud control plane.
cp -r /etc/kubernetes/pki /home/debian/
chmod -R 777 /home/debian/pki/

# Un token de join sans expiration. À créer avant de vous déconnecter.
kubeadm token create --ttl 0
# Depuis votre poste.
scp -r <region>-<env>-k8s-master-1:~/pki ./
talosctl gen secrets --kubernetes-bootstrap-token <token> --from-kubernetes-pki ./pki

Vérifiez que le répertoire extrait contient bien ca.{crt,key}, front-proxy-ca.{crt,key}, sa.{key,pub} et etcd/ca.{crt,key} avant de générer. Un bundle sans la CA etcd se génère très bien et échoue beaucoup plus tard.

Étape 3 — Importer le bundle dans OpenTofu

talosctl gen secrets a écrit un secrets.yaml. Il doit devenir de l’état, pas un fichier que quelqu’un a en local :

tofu import talos_machine_secrets.this ~/chemin/vers/secrets.yaml

Importer et non générer — si un tofu apply génère un jour cette ressource lui-même, il produit une CA neuve et vous avez discrètement construit un second cluster, vide, portant le nom du vôtre.

Étape 4 — Mettre à jour le driver CSI OpenStack Cinder

La version que nous faisions tourner montait /etc/cacert depuis l’hôte. Nous n’en avions jamais eu besoin, et sous Talos ça ne peut pas marcher : le système de fichiers racine est en lecture seule et ce chemin n’existe pas. Les pods restent en ContainerCreating, et le premier worker Talos que vous ajoutez ne peut attacher aucun volume. Appliquez le driver à jour depuis le dépôt de plateforme, sans les secrets SOPS, et recréez les workloads — les changements de pod spec ne passent pas par un rolling update :

# Controller plugin.
kubectl delete deployment openstack-cinder-csi-controllerplugin
kubectl apply -f bases/csi-driver/csidriver.yaml

# Un node plugin par zone de disponibilité.
kubectl delete daemonset <az1>-openstack-cinder-csi-nodeplugin
kubectl apply -f bases/csi-driver/nodeplugin-<az1>.yaml
# répéter par AZ

Faites-le tant que le cluster est encore entièrement kubeadm, vérifiez qu’un PVC se lie et qu’un pod le monte, puis passez à la suite. La généralisation mérite d’être écrite, parce que Cinder n’était pas le seul coupable : chaque hostPath dont dépendent vos add-ons est un bloqueur de migration. Des identifiants cloud lus dans /etc/kubernetes/cloud.conf, des bundles de CA dans /etc/ssl, tout ce qu’un rôle Kubespray a déposé sur un nœud — tout cela doit d’abord devenir un Secret ou une ConfigMap. Cherchez hostPath dans vos manifests avant de poser des dates.

Étape 5 — Donner une identité aux sondes de santé externes

Talos lance l’apiserver avec l’authentification anonyme désactivée. Nos clusters kubeadm l’avaient activée, et quelque chose s’appuyait dessus sans le dire : le traffic manager externe qui fait des GET /healthz non authentifiés pour décider si une région est vivante. Sur le premier control plane Talos, ces sondes deviennent des 401, la région est marquée hors service et le trafic quitte un cluster en parfaite santé.

Attachez les URLs de santé (non-resource) à une véritable identité, et faites pointer la sonde sur un token :

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: <tenant>-read-kube-health
  labels:
    app: traffic-manager-health
    managed-by: terraform
rules:
  - nonResourceURLs: ["/readyz", "/readyz/*", "/livez", "/livez/*", "/healthz"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: <tenant>-read-kube-health-static-token
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: <tenant>-read-kube-health
subjects:
  - kind: ServiceAccount
    name: <health-probe-sa>
    namespace: kube-system

Puis partez chercher les autres. Tout ce qui interroge l’apiserver sans s’authentifier — un vieux blackbox exporter, une sonde de load balancer, une page de statut — fonctionne aujourd’hui grâce à une configuration kubelet et apiserver que Talos ne reproduira pas. Trouvez-les sur kubeadm, pendant que c’est encore possible.

Étape 6 — Réécrire les values Cilium

Cilium a besoin de quatre choses spécifiques à Talos : KubePrism comme endpoint d’apiserver, pas d’automount de cgroup puisque Talos possède /sys/fs/cgroup et que la racine est en lecture seule, un jeu de capabilities explicite puisque rien n’est privilégié par défaut, et Envoy exclu si vous ne le faites pas tourner.

ipam:
  mode: kubernetes
kubeProxyReplacement: true
k8sServiceHost: localhost
k8sServicePort: 7445
cgroup:
  autoMount:
    enabled: false
  hostRoot: /sys/fs/cgroup
envoy:
  create: false
securityContext:
  capabilities:
    ciliumAgent:
      - CHOWN
      - KILL
      - NET_ADMIN
      - NET_RAW
      - IPC_LOCK
      - SYS_ADMIN
      - SYS_RESOURCE
      - DAC_OVERRIDE
      - FOWNER
      - SETGID
      - SETUID
    cleanCiliumState:
      - NET_ADMIN
      - SYS_ADMIN
      - SYS_RESOURCE

C’est l’étape qui casse la règle du « sans effet », et celle à laquelle il faut réfléchir le plus. k8sServiceHost: localhost et le port 7445 décrivent un proxy qui existe sur les nœuds Talos et qui n’existe pas sur un nœud kubeadm. Poussez ces values avec un rolling update par défaut et Helm redémarre le DaemonSet agent sur un cluster encore majoritairement kubeadm ; chaque agent qui redémarre cherche un apiserver sur un port loopback où rien n’écoute, et vous perdez le réseau sur des nœuds qui allaient bien une minute avant.

Réglez la stratégie de mise à jour du DaemonSet agent pour que la release ne redémarre pas les pods en cours — OnDelete, ou un rollout mis en pause avant qu’il ne touche quoi que ce soit — et laissez la nouvelle pod spec arriver sur chaque nœud au moment où ce nœud est remplacé. Nouveau nœud Talos, pod agent neuf, bonnes values. Les agents kubeadm gardent la spec avec laquelle ils ont démarré jusqu’à ce que leur nœud cesse d’exister. Vérifiez-le avec un kubectl rollout status qui refuse d’avancer avant d’y croire, pas après.

Étape 7 — Dérouler les nœuds

C’est seulement maintenant qu’un nœud change.

Control planes, strictement un à la fois. Trois membres etcd signifient que vous pouvez en perdre exactement un. Drainez le master kubeadm, retirez proprement son membre etcd, laissez OpenTofu provisionner le nœud Talos à sa place, puis attendez — talosctl -n <ip> etcd members et talosctl -n <ip> health — que le remplaçant soit membre votant et que l’apiserver derrière lui réponde. Alors seulement, touchez au deuxième. La tentation de parallélisation est ici l’erreur la plus coûteuse de toute la procédure ; deux membres perdus sur trois, c’est un cluster en lecture seule et une restauration de sauvegarde.

Les workers ensuite, par lots que vos PodDisruptionBudgets contrôlent réellement. Si un PDB est absent ou faux, le drain emportera sans hésiter le dernier replica de quelque chose. C’est là que les nœuds Talos lèvent leur propre agent Cilium avec les values de l’étape 6 et montent des volumes Cinder avec le driver de l’étape 4 : si l’un des deux est faux, vous l’apprenez sur le worker numéro un, avec tout le reste encore sur kubeadm. C’est le bon mode de défaillance, et c’est pour cela que l’ordre de ces notes est celui-là.

Gardez le dernier master kubeadm pour la fin. C’est le seul endroit où l’on peut encore lire l’ancienne configuration, comparer un manifest rendu, ou vérifier ce que valait un argument. Une fois qu’il est parti, il n’y a plus un seul shell dans le cluster.

Ensuite

Décommentez les providers kubernetes et helm, réimportez les ressources in-cluster dans l’état, et confirmez qu’un plan revient propre. Supprimez le token de bootstrap --ttl 0. Supprimez chaque copie du répertoire PKI. Supprimez la branche de préparation. Puis servez-vous de ce que vous venez de construire : remplacez un nœud sans aucune raison et regardez-le revenir en cinq minutes, parce que c’est la capacité que vous avez réellement achetée.

Ce que je ferais autrement

Deux choses.

Je grepperais hostPath et les accès non authentifiés à l’apiserver dès le premier jour, avant de proposer une date. Nos deux vraies surprises — le driver CSI et les sondes de santé — étaient dans cet ensemble, et toutes deux étaient trouvables par une recherche plutôt que par un déploiement. Une matinée de grep les aurait fait passer de « incident pendant la fenêtre » à « ligne dans le plan ».

Je vérifierais bien les sauts de version d’ETCD. N’ayant jamais eu à gérer de breaking changes dans etcd, je n’ai naïvement pas vérifié les changements incompatibles. En passant de Kubeadm à Talos, je n’avais pas vu qu’etcd devait faire un saut entre le backend de stockage v2 et le v3. Le saut de version que nous faisions lors de nos premiers essais cassait le cluster etcd. Renseignez-vous bien sur les sauts à effectuer.

Et bonne chance !

Vous exploitez quelque chose de similaire et hésitez à franchir le pas ? Je réalise des audits de fiabilité en deux semaines qui répondent exactement à cette question.

Travaillons ensemble