Contenu

Upgrade AKS : ce que l'audit ne voit pas

Le mythe de l'audit propre, la réalité des causes d'échec

À en croire le guide Azure, un upgrade AKS est une opération simple et sûre : on choisit la version cible, on clique sur “Upgrade” et Azure s’occupe du reste. On consulte donc l’audit des API dépréciées proposé par Azure, on réserve un créneau de quelques heures, puis le jour J, l’indisponibilité dure deux fois plus longtemps que prévu. La cause se trouve presque toujours là où l’audit n’a pas regardé.

Cet article n’a pas vocation à être exhaustif. Il s’agit de mon modeste retour d’expérience sur les problèmes que j’ai rencontrés lors d’upgrades AKS.

Azure propose un audit des API dépréciées, accessible depuis le menu “Diagnose and solve problems” du cluster (puis “Kubernetes API deprecations”). Il répond à une question précise : “cette version de Kubernetes retire-t-elle des API utilisées par ce cluster ?”. Pour cela, il s’appuie sur les appels à ces API observés sur le cluster, et AKS bloque d’ailleurs l’upgrade si de tels appels ont eu lieu dans les 12 heures précédentes.

Cette vérification est nécessaire, mais elle ne suffit pas. En effet, un upgrade AKS se déroule en deux temps : le control plane d’abord, puis les node pools. L’audit porte sur le premier, alors que les échecs se produisent presque tous pendant le second, au moment où les noeuds sont remplacés.

Nous allons voir ici les trois familles de causes d’échec que j’ai le plus souvent rencontrées.

Une montée de version avec surge provisionne des noeuds temporaires avant de retirer les anciens. Ce pic peut dépasser plusieurs limites, alors que le cluster les respecte largement en temps normal.

Si la famille de machine (Standard_DSv4, DSv5…) atteint son plafond régional sur l’abonnement, le provisioning échoue et l’opération se fige. Le quota total de vCPU de la région (Total Regional vCPUs) est un second plafond, distinct, qui peut bloquer le surge de la même façon. Un moniteur par famille de SKU et par région permet d’anticiper ce problème.

Avec Azure CNI classique, chaque noeud réserve dès sa création une adresse pour lui-même et une par pod possible (maxPods, 30 par défaut). Un noeud de surge consomme donc d’emblée 31 adresses, et le besoin total est de (noeuds + maxSurge) * (1 + maxPods). Il faut y ajouter les 5 adresses qu’Azure réserve dans chaque subnet. Sur un subnet dimensionné au plus juste, Azure refuse alors l’opération :

(SubnetIsFull) Code="SubnetIsFull" Message="Subnet <subnet> with address prefix <plage> does not have enough capacity for <n> IP addresses."

Avec Azure CNI Overlay, seuls les noeuds consomment des adresses du subnet et ce risque devient marginal.

Les machines spot dépendent de la capacité disponible dans la région, qui varie selon le SKU et la demande. Plus rarement, une famille de SKU peut aussi manquer de capacité dans une zone (AllocationFailed, SKUNotAvailable), quel que soit le quota de l’abonnement.

Avertissement
Un audit qui vérifie les quotas à un instant T ne valide pas la capacité du cluster à absorber le pic créé par l’upgrade. Azure contrôle désormais le quota et le subnet au lancement de l’opération, mais le refus tombe tout de même le jour J.

Pour vérifier ces limites avant l’opération :

# vCPU consommés et plafonds, par famille de SKU et pour toute la région
az vm list-usage --location <region> -o table | grep -iE 'family|total regional'

# Adresses utilisées et plafond par subnet (le plafond exclut déjà les 5 adresses réservées)
az rest --method get \
  --url "/subscriptions/{subscriptionId}/resourceGroups/<rg>/providers/Microsoft.Network/virtualNetworks/<vnet>/usages?api-version=2024-05-01" \
  | jq -r '["SUBNET","USED","LIMIT"], (.value[] | [(.id | split("/") | last), .currentValue, .limit]) | @tsv' \
  | column -t

Les nouveaux noeuds démarrent avec un cache d’images vide. Chaque pod replanifié refait donc un pull, et tous en même temps. Une application qui tournait parfaitement peut alors ne plus redémarrer, car elle dépend d’une image qu’elle ne peut plus récupérer.

Le premier cas est le rate limit de Docker Hub :

429 Too Many Requests - Server message: toomanyrequests:
You have reached your unauthenticated pull rate limit.

Docker Hub limite les pulls anonymes à 100 par adresse IP toutes les 6 heures. Or, les noeuds d’un cluster AKS sortent sur Internet par les mêmes quelques IP publiques. Un simple init container basé sur debian:bookworm-slim suffit donc à épuiser ce quota en quelques minutes, et les pods passent en ImagePullBackOff les uns après les autres.

Deux autres cas sont masqués de la même façon par le cache des anciens noeuds :

  • une image supprimée d’un registre privé alors qu’un déploiement la référence encore ;
  • un pare-feu qui bloque l’accès aux registres publics.

Toute image doit donc provenir d’un registre que l’on maîtrise, et y rester tant qu’elle est référencée.

Les commandes suivantes listent les images qui ne proviennent ni d’un ACR ni de MCR. La première ne voit néanmoins que les pods existants : les CronJobs qui ne tournent pas au moment du contrôle sont donc vérifiés avec la seconde, à partir de leur modèle de pod :

# Images des pods existants
kubectl get pods -A -o json | jq -r '
  .items[].spec | ((.initContainers // []) + .containers)[] | .image' \
  | sort -u | grep -vE '\.azurecr\.io/|^mcr\.microsoft\.com/'

# Images des CronJobs, qui ne tournent pas forcément au moment du contrôle
kubectl get cronjobs -A -o json | jq -r '
  .items[].spec.jobTemplate.spec.template.spec | ((.initContainers // []) + .containers)[] | .image' \
  | sort -u | grep -vE '\.azurecr\.io/|^mcr\.microsoft\.com/'

Un upgrade AKS vide les noeuds par lots (cordon puis drain) et recrée ailleurs chaque pod évincé. Pour une application qui ne tourne que sur un seul pod, chaque drain du noeud qui l’héberge provoque donc une coupure de service. Elle peut même en subir plusieurs, si son pod est replanifié sur un noeud pas encore traité ou sur un noeud temporaire supprimé en fin d’opération.

La coupure cumule l’éviction, la planification, le pull de l’image et le démarrage de l’application. Elle s’allonge encore avec un volume persistant, car le disque Azure doit être détaché puis rattaché, ce qui prend plusieurs minutes. De plus, un disque LRS est lié à une zone : si aucun nouveau noeud n’est disponible dans cette zone, le pod reste en Pending (un disque ZRS lève cette contrainte).

Le diagramme suivant illustre les coupures successives d’une application mono-pod avec un volume :

gantt
    title Coupures d'une application mono-pod avec volume (durées indicatives)
    dateFormat HH:mm
    axisFormat %H:%M
    section 1er drain
    Éviction                          :crit, a1, 00:00, 00:01
    Planification                     :crit, a2, 00:01, 00:02
    Détachement et rattachement disque :crit, a3, 00:02, 00:07
    Pull de l'image                   :crit, a4, 00:07, 00:09
    Démarrage applicatif              :crit, a5, 00:09, 00:11
    section Service rétabli
    Pod replanifié sur un ancien noeud :active, b1, 00:11, 00:18
    section 2e drain
    Nouvelle coupure complète         :crit, c1, 00:18, 00:29

Enfin, ces applications peuvent bloquer l’opération lorsqu’elles sont protégées par un PodDisruptionBudget (PDB), qui fixe le nombre minimum de pods disponibles pendant une éviction. Un minAvailable: 1 (ou maxUnavailable: 0) sur un déploiement à un seul replica donne mécaniquement un ALLOWED DISRUPTIONS à zéro : le noeud ne peut pas être vidé et l’upgrade est bloqué.

Il s’agit rarement d’oublis : composants avec élection de leader, bases de données en StatefulSet à un replica, applications dimensionnées au minimum hors production ou volumes en ReadWriteOnce. L’audit ne les détecte pas, car ils sont parfaitement compatibles avec la version cible.

Pour lister les workloads à un seul replica :

kubectl get deploy,statefulset -A -o json | jq -r '
  .items[] | select(.spec.replicas == 1)
  | [.metadata.namespace, .kind, .metadata.name] | @tsv'

Le tableau suivant résume, pour chaque famille, ce qu’il faut vérifier avant l’opération et la solution durable à mettre en place :

Famille À vérifier avant l’opération Solution durable
Quotas et capacité vCPU par famille de SKU, adresses libres du subnet, capacité spot maxSurge dimensionné sur la capacité réelle, Azure CNI Overlay
Images de conteneurs Images hors registre privé, tags supprimés Registre privé ou cache ACR pour toutes les images, init containers compris
Applications mono-pod Workloads à un replica, volumes ReadWriteOnce Plusieurs replicas et topologySpreadConstraints, sinon coupure annoncée

AKS propose également plusieurs options pour limiter les risques :

  • az aks upgrade --control-plane-only puis az aks nodepool upgrade : traiter le control plane, puis les node pools un par un ;
  • --max-surge 0 et --max-unavailable : réaliser l’upgrade sans noeud de surge, au prix d’une capacité réduite (hors system node pools) ;
  • --drain-timeout et --undrainable-node-behavior Cordon : réduire l’attente sur un drain bloqué, ou mettre le noeud en quarantaine plutôt que de faire échouer l’upgrade ;
  • --enable-force-upgrade : ignorer les PDB, en dernier recours.

D’autres points ne se révèlent qu’au remplacement des noeuds et peuvent, eux aussi, être vérifiés avant l’opération :

  • les webhooks d’admission : si le contrôleur (Kyverno par exemple) est évincé et que le webhook est en failurePolicy: Fail, plus aucun pod applicatif ne peut démarrer. Plusieurs replicas du contrôleur et une failurePolicy: Ignore pour les politiques non critiques évitent ce blocage ;
  • les PodDisruptionBudgets : un PDB trop strict, ou des pods déjà en échec, mettent ALLOWED DISRUPTIONS à zéro et bloquent le drain. unhealthyPodEvictionPolicy: AlwaysAllow autorise l’éviction des pods déjà en échec ;
  • l’état de santé du cluster : sans photo de l’état initial, il est impossible de distinguer ce que l’upgrade a cassé de ce qui l’était déjà ;
  • le chemin de version : hors versions LTS, AKS impose de monter une version mineure à la fois, sans retour arrière possible ;
  • les identités et la gouvernance Azure : un secret de service principal expiré, une Azure Policy en deny ou un verrou sur le resource group des noeuds bloquent la création des nouveaux noeuds ;
  • l’image des noeuds : le passage d’Ubuntu 22.04 à 24.04 à partir de Kubernetes 1.35 peut perturber les agents installés sur les noeuds ;
  • les composants tiers : contrôleur ingress, cert-manager ou Istio publient chacun leur matrice de compatibilité.

Enfin, il est important d’harmoniser les environnements, ou au moins de documenter leurs écarts. Un webhook présent en qualification mais absent en développement fait par exemple échouer l’upgrade en qualification seulement.

Les commandes permettant de vérifier certains de ces points sont regroupées ci-dessous :

# Webhooks d'admission et leur failurePolicy
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations \
  -o custom-columns='NAME:.metadata.name,FAILURE_POLICY:.webhooks[*].failurePolicy'

# PDB qui bloquent actuellement toute éviction
kubectl get pdb -A -o json | jq -r '
  .items[] | select(.status.disruptionsAllowed == 0)
  | [.metadata.namespace, .metadata.name] | @tsv'

# État de départ : cluster, noeuds, pods non prêts
az aks show -g <rg> -n <cluster> --query provisioningState
kubectl get nodes
kubectl get pods -A -o json | jq -r '.items[]
  | select(.status.phase != "Succeeded")
  | select(.status.phase != "Running" or any(.status.containerStatuses[]?; .ready | not))
  | [.metadata.namespace, .metadata.name, .status.phase] | @tsv'

# Versions cibles disponibles
az aks get-upgrades -g <rg> -n <cluster> -o table

# Verrous sur le resource group des noeuds, expiration du service principal
az lock list -g <node-resource-group> -o table
az ad app credential list --id <client-id> --query "[].endDateTime"

L’audit de dépréciation reste nécessaire, mais il ne suffit pas à garantir le bon déroulement d’un upgrade AKS. Avant de planifier le créneau, il faut aussi se demander si l’infrastructure et les applications peuvent absorber l’opération.

Cette question va d’ailleurs devenir incontournable. À partir du 1er juin 2027, AKS upgradera lui-même tout cluster resté sur une version non supportée au-delà de la période de grâce de 60 jours, sans respecter la fenêtre de maintenance ni les PDB qui bloquent un drain.

Mieux vaut donc traiter ces points dès maintenant, avant qu’AKS ne choisisse la date de l’upgrade à votre place !

Pour terminer, voici une checklist récapitulative à passer en revue avant chaque upgrade :

  • quota de vCPU : la famille de SKU et le total régional disposent de suffisamment de vCPU libres pour absorber les noeuds de surge ;
  • adresses IP : le subnet peut accueillir (noeuds + maxSurge) * (1 + maxPods) adresses avec Azure CNI classique, plus les 5 réservées par Azure ;
  • registres d’images : toutes les images, init containers et CronJobs compris, proviennent d’un registre que l’on maîtrise et y sont toujours présentes ;
  • PDB et applications mono-pod : aucun PDB ne bloque toute éviction (ALLOWED DISRUPTIONS à zéro), et la coupure des workloads à un seul replica est annoncée ;
  • volumes persistants : les pods avec un disque Azure sont identifiés, et des noeuds sont disponibles dans la zone de chaque disque LRS.