Featured image of post Kyverno a tué mon API Server. Encore.

Kyverno a tué mon API Server. Encore.

Ecrit par ~ zwindler ~

Le retour de la vengeance

Vous avez peut-être lu mon article précédent sur etcd il y a 3 ans. Si vous vous en souvenez bien, le crash, c’était etcd, mais le vrai coupable, c’était Kyverno. J’adore Kyverno. C’est vraiment un logiciel que j’aime beaucoup. Notamment parce que c’est puissant(issime). J’ai d’ailleurs écrit un article d’intro et un second qui va plus loin sur le sujet.

Mais le nombre d’incidents et de side effects chelou que ça provoque. Mamamia… C’est pas le premier incident de l’année que j’ai avec Kyverno (oui, on est en février) mais comme celui-là est rigolo, je vous le partage.

Lors d’une opération de maintenance de routine pour mettre à jour un cluster Kubernetes vers la version 1.34 (depuis la 1.32), on s’est retrouvé face au scénario redouté par tout admin kube : un API Server totalement injoignable au redémarrage des nœuds du Control Plane.

Ce qui ressemblait initialement à une erreur réseau classique s’est avéré être un deadlock subtil entre les nouvelles fonctionnalités réseau natives de Kubernetes et notre cher Kyverno 😘.

Spoiler : c’était pas un problème réseau. C’est jamais un problème réseau. Enfin si, des fois. Mais pas cette fois.

L’upgrade qui commence bien

Bon, un upgrade de Kubernetes, c’est devenu assez banal à ce stade. On fait ça régulièrement, on a nos procédures, on est des pros (si si). On passe de la 1.32 à la 1.34 en un seul commit, sans faire le petit saut par la 1.33 comme recommandé.

TKT FRR.

Dans le contexte technique dont je parle, tout est géré as code. De la création des machines jusqu’au déploiement de Talos, en passant par les MachineConfig (les CustomResources pour modifier… et bien, la machine).

Pour plus d’infos, voir la documentation Talos sur les Machine Configs.

Le premier cluster qu’on teste n’a qu’un seul control plane (me demandez pas pourquoi, ça n’aurait probablement rien changé). Talos relance l’API Server dans la nouvelle version et là… rien.

Les logs de l’API Server “chelous” (terme technique) parlent d’eux-mêmes :

I0224 15:32:50.979280  1 default_servicecidr_controller.go:166] Creating default ServiceCIDR with CIDRs: [10.1.0.0/20]
W0224 15:32:50.984784  1 dispatcher.go:225] rejected by webhook "validate.kyverno.svc-fail":
  admission webhook "validate.kyverno.svc-fail" denied the request:
  Get "https://10.1.0.1:443/api": dial tcp 10.1.0.1:443: connect: operation not permitted
I0224 15:32:50.985342  1 event.go:389] "Event occurred" kind="ServiceCIDR"
  apiVersion="networking.k8s.io/v1" type="Warning"
  reason="KubernetesDefaultServiceCIDRError"
  message="The default ServiceCIDR can not be created"

😬😬😬

La root cause : un magnifique cercle vicieux

Après investigation, on a découvert que l’incident était le résultat d’une collision entre une évolution du core de Kubernetes et notre configuration Kyverno. Un beau cas d’école de deadlock.

Décomposons le mécanisme :

  1. Le nouveau Kind ServiceCIDR

Dans les versions récentes (v1.33+), Kubernetes migre la gestion des plages d’IP de services vers des objets dédiés nommés ServiceCIDR. Au premier démarrage après l’upgrade, l’API Server tente de créer automatiquement l’objet par défaut (par exemple 10.1.0.0/20).

Pour les curieux, la KEP-1880 et la documentation officielle du ServiceCIDR détaillent cette évolution.

C’est nouveau, c’est propre, c’est bien pensé. Sauf que…

  1. Interception par le Webhook Kyverno

Kyverno, configuré avec une failurePolicy: Fail (parce qu’on est des gens sérieux qui ne laissent pas passer n’importe quoi en prod), est programmé pour intercepter les créations de ressources afin de les valider, et faire échouer le call si Kyverno ne répond pas.

Y compris le ServiceCIDR fraîchement créé par l’API Server lui-même.

  1. Deadlock

Et c’est là que ça devient magnifique (“la douleur peut être une source de plaisir” - Mozinor) :

  • L’API Server suspend la création du ServiceCIDR en attendant le “OK” de Kyverno
  • Pour contacter le service Kyverno, l’API Server doit router la requête via l’IP du service Kubernetes (en général 10.1.0.1)
  • Sauf que la couche réseau (routage vers les services) ne peut pas s’initialiser tant que l’objet ServiceCIDR n’est pas validé et créé

C’est l’œuf et la poule, version “j’ai les clés de la voiture dans la voiture fermée à clé”.

PTSD. Oui ça m’est arrivé. Dans le désert. Sans réseau.

  1. Profit.

L’API Server tombe en timeout ou renvoie une erreur connect: operation not permitted en tentant de joindre le webhook, bloquant ainsi sa propre initialisation. CrashLoopBackOff sur l’API Server. :D

Sortir du deadlock

Pour sortir de ce deadlock, il faut bypasser temporairement la couche d’admission. Facile, non ?

Le workaround “habituel” : inutile

Les deadlocks avec Kyverno, on a l’habitude à ce stade. Normalement, comme kube-system est ignoré, on peut simplement se connecter avec un kube-config de secours (breaking glass, on est en OIDC normalement) qui a le cluster role cluster-admin et supprimer les validating webhooks de Kyverno :

kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg

Sauf que là, c’est l’API Server qui ne démarre même pas. Mon kubectl ne risque pas de fonctionner !

Le vrai workaround : désactiver les webhooks au boot

La solution qu’on a choisie a été de modifier la configuration de l’API Server pour désactiver temporairement les webhooks de validation au démarrage. Mon éminent collègue Maxime a édité à chaud le machine config (avec un accès talosctl breaking glass) pour ajouter le flag suivant directement dans les extraArgs de l’API server :

--disable-admission-plugins=ValidatingAdmissionWebhook

Pour ceux qui ne savent pas trop comment ça fonctionne l’admission control dans Kubernetes, sachez juste qu’il y a une liste de plugins “par défaut” mais que tout est débrayable. Je ferai peut-être un jour un deep dive sur l’admission control dans Kubernetes, c’est fascinant ;).

Avec ce flag, l’API Server peut enfin créer ses objets ServiceCIDR sans demander la permission à personne (on bypass complètement tous les mécanismes de validation que Kyverno ou assimilé enforce), le réseau s’initialise, Kyverno démarre, et on peut ensuite retirer le flag et relancer proprement.

L’option “golri” qu’on n’a pas testée

Personnellement, je trouvais très rigolo l’idée d’aller modifier directement la base etcd pour supprimer la clé du webhook qui posait problème (là aussi en passant par talosctl) :

# Exemple via etcdctl
etcdctl del /registry/admissionregistration.k8s.io/validatingwebhookconfigurations/kyverno-resource-validating-webhook-cfg

Mes collègues étaient moins chauds : “Ouais mais tu comprends, si on casse etcd ça va être pénible”. On a joué la sécurité avec le flag. Je suis super déçu, on n’a pas testé 😂.

La solution permanente : les MatchConditions

OK, maintenant que le cluster est reparti, comment on s’assure que ça ne se reproduise pas au prochain upgrade ?

La solution propre consiste à utiliser les matchConditions (introduites en Kubernetes 1.27) au niveau de la ValidatingWebhookConfiguration. Ça permet d’exclure les ressources critiques de bootstrap réseau avant même que la requête ne tente de sortir de l’API Server vers le pod Kyverno.

Voir la doc officielle sur les matchConditions.

On utilisait déjà cette option pour limiter le trafic parfois abusif de Kyverno (si vous administrez du Kyverno, vous savez de quoi je parle) sur un certain nombre d’événements (on écroule l’API server et/ou Kyverno en CPU ou RAM, selon les cas). Il a suffi d’ajouter les exclusions pour les nouveaux types :

# Exclusion des ressources de bootstrap réseau pour éviter le deadlock
matchConditions:
  - name: 'exclude-ServiceCIDR'
    expression: '!(request.kind.kind == "ServiceCIDR")'
  - name: 'exclude-IPAddress'
    expression: '!(request.kind.kind == "IPAddress")'

Avec ça, quand l’API Server crée un ServiceCIDR au boot, la requête ne passe même plus par le webhook Kyverno. Pas de dépendance circulaire, pas de deadlock, tout le monde est content.

Conclusion

Comme dirait notre cher (non) président :

qui aurait pu prévoir ?

Bon ok, il suffisait de lire les release notes de Kubernetes 1.33. Cela étant dit, on a un cluster de staging, il sert à ça. On a cassé staging, no big deal

Peut-être qu’on lira quand même, la prochaine fois ?

Licensed under CC BY-SA 4.0

Vous aimez ce blog ou cet article ? Partagez-le avec vos amis !   Twitter Linkedin email Facebook

Vous pouvez également vous abonner à la mailing list des articles ici

L'intégralité du contenu appartenant à Denis Germain (alias zwindler) présent sur ce blog, incluant les textes, le code, les images, les schémas et les supports de talks de conf, sont distribués sous la licence CC BY-SA 4.0.

Les autres contenus (thème du blog, police de caractères, logos d'entreprises, articles invités...) restent soumis à leur propre licence ou à défaut, au droit d'auteur. Plus d'informations dans les Mentions Légales

Généré avec Hugo
Thème Stack conçu par Jimmy