Aller au contenu
Conteneurs & Orchestration medium

Karpenter : l'autoscaler Kubernetes nouvelle génération

26 min de lecture

Vos pods restent bloqués en Pending parce que le cluster manque de capacité. Karpenter détecte ces pods en attente et déclenche le provisionnement en quelques secondes, mais le temps jusqu'à un nœud Ready dépend du provider cloud, du bootstrap (OS, runtime, CNI) et des pulls d'images. En pratique, comptez 1 à 4 minutes selon votre environnement.

Les nœuds sont dimensionnés exactement pour vos pods et disparaissent automatiquement quand ils ne servent plus. Ce guide vous donne le modèle mental pour comprendre comment Karpenter fonctionne, son architecture interne, et pourquoi il surpasse les autoscalers traditionnels.

  • Comprendre les ressources clés : NodePool, NodeClass, NodeClaim, NodeOverlay
  • Maîtriser la Disruption : consolidation, drift, expiration, budgets
  • Comprendre le Scheduling : comment Karpenter simule et sélectionne les instances
  • Visualiser l'architecture interne de Karpenter et le rôle de chaque contrôleur
  • Distinguer ce que fait Karpenter vs ce que fait kube-scheduler
  • Comparer Karpenter avec Cluster Autoscaler pour choisir le bon outil

Dans un cluster Kubernetes traditionnel, l'autoscaling des nœuds repose sur le Cluster Autoscaler. Ce dernier ajuste le nombre de nœuds au sein de groupes de nœuds (Node Groups ou Auto Scaling Groups). Cette approche présente plusieurs limitations :

LimitationImpact
RigiditéVous devez créer et maintenir de nombreux groupes de nœuds pour différents types d'instances
LenteurL'ajout d'un nœud peut prendre plusieurs minutes (négociation avec ASG, attente de scaling)
InefficacitéLa sélection du type d'instance est limitée au groupe, pas optimisée pour le pod
ComplexitéGérer des centaines de groupes devient vite ingérable

Karpenter élimine ces contraintes en provisionnant directement les instances sans passer par des groupes intermédiaires. Il analyse les pods en attente, détermine le type d'instance optimal parmi tous ceux disponibles, et crée immédiatement la capacité nécessaire.

Le critère décisif est la variabilité de vos besoins en capacité. Sur un cluster dont les workloads sont homogènes et la charge prévisible, un Cluster Autoscaler avec deux ou trois groupes de nœuds fait le travail sans ajouter de CRD ni de contrôleur à maintenir. Karpenter prend l'avantage dès que le nombre de combinaisons pertinentes (architecture, taille, spot ou on-demand, zone) rend la maintenance des groupes déraisonnable.

  • Vos workloads ont des besoins variés (architectures, types d'instances, spot/on-demand)
  • Vous voulez une consolidation automatique des nœuds sous-utilisés
  • La réactivité compte (pods schedulés en secondes, pas en minutes)
  • Vous déployez sur un cloud avec un provider Karpenter disponible

Karpenter sépare un cœur générique, maintenu par le projet Kubernetes SIG, et un provider par cloud qui implémente l'interface CloudProvider. Cette architecture explique la colonne NodeClass du tableau : chaque provider apporte sa propre CRD de configuration cloud, alors que NodePool et NodeClaim restent identiques partout. La colonne Maturité mérite votre attention avant tout choix : un provider en Preview peut voir son API changer sans préavis, ce qui n'est pas anodin sur un composant qui crée et détruit des machines.

ProviderMaintenu parMaturitéNodeClassRepo
AWSAWSProductionEC2NodeClassaws/karpenter-provider-aws
Azure/AKSMicrosoftProductionAKSNodeClassAzure/karpenter-provider-azure
GCP/GKECloudPilot AIPreviewGCPNodeClasscloudpilot-ai/karpenter-provider-gcp
Alibaba CloudCloudPilot AIPreviewECSNodeClasscloudpilot-ai/karpenter-provider-alibabacloud

Karpenter utilise trois Custom Resource Definitions (CRDs) qui travaillent ensemble. Comprendre leur rôle est essentiel pour configurer et dépanner Karpenter.

Karpenter CRDs

La NodeClass répond à la question : "Comment créer un nœud chez mon provider ?"

Chaque provider cloud a sa propre NodeClass (EC2NodeClass pour AWS, AKSNodeClass pour Azure). Elle contient :

  • Image machine : AMI sur AWS, OMI sur Outscale
  • Réseau : subnets, security groups
  • IAM/Credentials : rôle pour le nœud
  • User-data : script d'initialisation
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
role: "KarpenterNodeRole-my-cluster"
amiSelectorTerms:
- alias: al2023@latest
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "my-cluster"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "my-cluster"

Un point de vigilance sur alias: al2023@latest : la documentation du provider AWS déconseille explicitement latest en production. Chaque publication d'une nouvelle AMI rend vos nœuds existants « driftés », et Karpenter les remplace en cascade. En production, épinglez une version d'AMI précise, par exemple al2023@v20240625, et faites du remplacement une opération volontaire.

Le NodePool répond à la question : "Quels types de nœuds Karpenter peut-il créer ?"

Il définit :

  • Requirements : architectures autorisées, types d'instances, zones de disponibilité
  • Taints : restrictions sur les pods pouvant être schedulés
  • Limites : capacité maximale du pool (CPU, mémoire)
  • Disruption : comment et quand Karpenter peut supprimer des nœuds
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws # ou karpenter.outscale.com, etc.
kind: EC2NodeClass
name: default
requirements:
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
limits:
cpu: 100
memory: 200Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m

Le NodeClaim est une ressource interne créée automatiquement par Karpenter. Il représente une "demande de nœud" avec des spécifications précises. Vous ne créez jamais de NodeClaim manuellement.

Un NodeClaim contient :

  • Le type d'instance choisi
  • La zone de disponibilité
  • Le providerID (identifiant cloud de l'instance)
  • Les conditions de progression (Launched, Registered, Initialized, Ready)
# Exemple de NodeClaim créé automatiquement
apiVersion: karpenter.sh/v1
kind: NodeClaim
metadata:
name: default-abc123
labels:
karpenter.sh/nodepool: default
spec:
requirements:
- key: topology.kubernetes.io/zone
operator: In
values: ["eu-west-2a"]
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large"]
status:
providerID: aws:///eu-west-2a/i-0123456789abcdef0
conditions:
- type: Ready
status: "True"

Karpenter se compose de plusieurs contrôleurs qui travaillent ensemble pour gérer le cycle de vie complet des nœuds.

Architecture Karpenter : 4 contrôleurs et CloudProvider Interface

Le Provisioner Controller est le cœur du provisionnement. Il surveille en permanence les pods marqués comme Unschedulable par kube-scheduler et décide quels nœuds créer.

Flux de travail :

  1. Observation : écoute les événements pods via l'API Kubernetes
  2. Batching : regroupe les pods pending pendant ~10 secondes max (configurable via --batch-max-duration)
  3. Simulation : simule le scheduling pour déterminer les NodeClaims nécessaires
  4. Création : crée les ressources NodeClaim correspondantes

Le batching est crucial : au lieu de créer un nœud par pod, Karpenter attend jusqu'à 10 secondes (1s idle par défaut) pour regrouper plusieurs pods sur le même nœud si possible.

Le Disruption Controller optimise le cluster en identifiant les nœuds à supprimer ou remplacer. Il opère selon un ordre de priorité :

  1. Drift : nœuds dont la configuration ne correspond plus au NodePool
  2. Consolidation : nœuds vides ou sous-utilisés
  3. Expiration : nœuds ayant dépassé leur durée de vie (expireAfter)

Types de consolidation :

TypeDescription
EmptySupprime les nœuds sans pods applicatifs (en parallèle)
Multi-NodeMigre les pods de plusieurs nœuds vers moins de nœuds
Single-NodeRemplace un nœud par un type d'instance moins cher

Le Garbage Collection Controller vérifie périodiquement (~30 secondes) que chaque NodeClaim correspond à une VM réelle dans le cloud en appelant CloudProvider.List().

Fonctionnement :

  1. Appelle List() qui retourne toutes les VMs gérées par Karpenter
  2. Compare cette liste avec les NodeClaims existants
  3. Si un NodeClaim n'a pas de VM correspondante → suppression immédiate

Quand un nœud doit être supprimé, le Termination Controller gère la séquence de shutdown gracieux :

  1. Taint : ajoute karpenter.sh/disrupted:NoSchedule pour empêcher de nouveaux pods
  2. Drain : évince les pods via l'API Eviction (respecte les PDBs)
  3. Cleanup : attend que les VolumeAttachments soient supprimés
  4. Delete : appelle le cloud provider pour terminer l'instance
  5. Finalize : retire le finalizer pour permettre la suppression de l'objet Node

Point critique : Karpenter provisionne des nœuds, mais c'est kube-scheduler qui schedule les pods dessus.

Flux de provisionnement Karpenter : du pod Pending au node Ready

  1. Un pod est créé → kube-scheduler le marque Unschedulable (pas de nœud disponible)

  2. Karpenter observe ce pod Pending et analyse ses contraintes

  3. Karpenter crée un NodeClaim → l'API cloud provisionne l'instance

  4. Le kubelet s'enregistre → le node rejoint le cluster

  5. kube-scheduler (pas Karpenter) schedule le pod sur le nouveau nœud

Comprendre les phases d'un NodeClaim est essentiel pour le debugging. Ces conditions sont cumulatives et se lisent dans kubectl describe nodeclaim <nom> : la condition qui reste à Unknown désigne l'étape où le nœud s'est arrêté, et donc la couche à interroger. Un blocage sur Launched renvoie vers l'API du cloud (quota, capacité insuffisante dans la zone), un blocage sur Registered renvoie vers le bootstrap du kubelet (user-data, réseau, credentials), un blocage sur Initialized renvoie vers le CNI et les DaemonSets.

PhaseConditionQue se passe-t-il ?
Created-Karpenter a détecté des pods pending et créé un objet NodeClaim
LaunchedLaunched=TrueLe provider cloud a créé la VM (instance ID connu)
RegisteredRegistered=TrueLe kubelet s'est connecté et Karpenter a lié le Node au NodeClaim
InitializedInitialized=TrueLe nœud est Ready au sens Kubernetes (CNI, runtime, DaemonSets critiques)
ReadyReady=TrueLes taints d'initialisation sont retirées, les pods applicatifs peuvent être schedulés

La Disruption est le mécanisme par lequel Karpenter supprime ou remplace des nœuds. Elle se divise en deux catégories : les méthodes graceful (respectent les budgets) et les méthodes forceful (drainent immédiatement).

Ces deux méthodes sont déclenchées par une décision de Karpenter, pas par un événement subi. Comme rien ne presse, elles passent par les disruption budgets : Karpenter n'agit que sur la proportion de nœuds que vous l'autorisez à perturber en même temps. C'est ce qui les rend sûres à laisser actives en production.

MéthodeDéclencheurDescription
DriftNodePool/NodeClass modifiésRemplace les nœuds dont la config ne correspond plus
ConsolidationSous-utilisationRéduit les coûts en fusionnant les workloads

La consolidation s'exécute selon trois stratégies (dans cet ordre) :

  1. Empty Node, Supprime les nœuds vides en parallèle
  2. Multi-Node, Migre les pods de N nœuds vers moins de nœuds
  3. Single-Node, Remplace un nœud par un type moins cher
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized # ou WhenEmpty
consolidateAfter: 5m # attendre 5 min avant de considérer le nœud

Le drift est le mécanisme qui applique un changement de configuration aux nœuds déjà en service. Sans lui, modifier un NodePool n'affecterait que les nœuds créés ensuite, et votre cluster accumulerait des générations de nœuds divergentes. Karpenter détecte automatiquement si un NodeClaim ne correspond plus à son NodePool ou à sa NodeClass :

  • NodePool : changements dans spec.template.spec.requirements
  • NodeClass : changements dans amiSelectorTerms, subnetSelectorTerms, securityGroupSelectorTerms

Les nœuds driftés sont remplacés progressivement selon les budgets. C'est précisément ce qui rend alias: al2023@latest dangereux en production : une nouvelle AMI publiée par AWS marque d'un coup tous vos nœuds comme driftés.

À l'inverse, ces trois méthodes répondent à une contrainte extérieure : le cloud vous reprend l'instance Spot dans deux minutes, ou le nœud a dépassé la durée de vie que vous avez fixée. Attendre un budget n'aurait pas de sens, Karpenter draine donc immédiatement. Conséquence pratique à anticiper : vos PodDisruptionBudgets ne suffisent pas à protéger un service critique de ces événements, seule une répartition sur plusieurs zones et types de capacité le fait.

MéthodeDéclencheurDescription
ExpirationexpireAfter atteintForce le remplacement après une durée max
InterruptionSpot interruption, maintenance AWSÉvacuation préventive avant interruption cloud
Node Auto RepairNode unhealthy > 30 minRemplace les nœuds défectueux (alpha)
apiVersion: karpenter.sh/v1
kind: NodePool
spec:
template:
spec:
expireAfter: 720h # 30 jours max
terminationGracePeriod: 1h # temps max pour drainer

Un budget limite le nombre de nœuds perturbés simultanément. La valeur peut s'écrire en pourcentage ou en nombre absolu, et plusieurs budgets coexistent : Karpenter applique toujours le plus restrictif de ceux qui s'appliquent à l'instant considéré. Le champ reasons cible une cause précise, ce qui permet d'être tolérant sur la consolidation tout en freinant le drift, sachant que les seules valeurs acceptées sont Underutilized, Empty et Drifted. Le troisième budget de l'exemple illustre le cas d'usage le plus courant : nodes: "0" combiné à schedule et duration gèle toute disruption pendant une fenêtre de maintenance. Si vous ne déclarez aucun budget, Karpenter en applique un par défaut à 10 %.

apiVersion: karpenter.sh/v1
kind: NodePool
spec:
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
budgets:
- nodes: "20%" # max 20% des nœuds en disruption
reasons: ["Drifted", "Empty"]
- nodes: "5" # plafond absolu
- nodes: "0" # bloquer pendant maintenance
schedule: "@daily"
duration: 30m
reasons: ["Underutilized"]

Les budgets raisonnent à l'échelle du NodePool. Pour protéger un cas individuel, par exemple un job de migration de base qui ne supporte pas d'être interrompu, l'annotation ci-dessous agit au niveau du pod ou du nœud. Posée sur un pod, elle rend le nœud qui l'héberge non perturbable tant que ce pod tourne, ce qui est la forme la plus utile : la protection disparaît d'elle-même quand le travail est terminé.

AnnotationEffet
karpenter.sh/do-not-disrupt: "true" sur podBloque la disruption volontaire du nœud
karpenter.sh/do-not-disrupt: "true" sur nodeBloque toute disruption volontaire

Quand Karpenter doit créer un nœud, il simule le scheduling pour choisir le type d'instance optimal.

Ce qui distingue Karpenter du Cluster Autoscaler tient dans l'étape 2 : au lieu de choisir un groupe de nœuds préexistant, Karpenter rejoue le scheduling en mémoire sur chaque type d'instance candidat pour savoir combien de pods y tiendraient réellement, en tenant compte des DaemonSets et de la réservation du kubelet. Le scoring de l'étape 3 arbitre ensuite sur le coût horaire. C'est pour cela qu'un NodePool trop restrictif dégrade la qualité des décisions : moins il reste de candidats après filtrage, moins la simulation a de marge.

  1. Filtrage, Élimine les types incompatibles avec les requirements
  2. Simulation, Calcule combien de pods pending tiennent sur chaque type
  3. Scoring, Choisit le type le moins cher qui satisfait les contraintes
  4. Batching, Regroupe les pods pendant ~10s max (configurable)

Les requirements dans le NodePool filtrent les types d'instances disponibles. Ces clés sont des labels standard de Kubernetes, à l'exception de karpenter.sh/capacity-type qui est propre à Karpenter : vos pods peuvent donc utiliser les mêmes clés dans leur nodeSelector ou leur nodeAffinity, et Karpenter en tiendra compte lors de la simulation. Un piège classique consiste à figer node.kubernetes.io/instance-type sur une seule valeur : vous retrouvez alors la rigidité d'un groupe de nœuds, sans les bénéfices de Karpenter.

LabelDescriptionExemple
kubernetes.io/archArchitecture CPUamd64, arm64
kubernetes.io/osSystème d'exploitationlinux, windows
node.kubernetes.io/instance-typeType exactm5.large, c6i.xlarge
topology.kubernetes.io/zoneZone de disponibilitéeu-west-1a
karpenter.sh/capacity-typeSpot ou On-Demandspot, on-demand

Quand plusieurs NodePools peuvent satisfaire un pod, Karpenter utilise le weight : il essaie d'abord le pool au poids le plus élevé, et bascule sur le suivant si le provisionnement échoue. L'exemple ci-dessous met en place le schéma le plus répandu, une bascule spot vers on-demand : tant que le cloud a de la capacité Spot, elle est utilisée ; en cas de refus, le pool on-demand-fallback prend le relais. Sans ce second pool, vos pods resteraient Pending pendant une pénurie de capacité Spot.

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: spot-pool
spec:
weight: 100 # Priorité haute → essayé en premier
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot"]
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: on-demand-fallback
spec:
weight: 10 # Priorité basse → fallback
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand"]

Les décisions de Karpenter valent ce que valent les données d'entrée de sa simulation : le catalogue de types d'instances, leurs capacités et leurs prix, tels que le provider les remonte. Le NodeOverlay est le point d'injection qui permet de corriger ce catalogue quand il ne correspond pas à votre réalité, sans modifier le provider lui-même.

Les NodeOverlays permettent de modifier les informations d'instances que Karpenter utilise pour ses décisions :

  • Ajuster les prix pour refléter vos Savings Plans ou contrats
  • Ajouter des extended resources non détectées automatiquement

Karpenter choisit ses instances sur les prix publics à la demande publiés par le provider. Si vous bénéficiez d'un Savings Plan ou d'un contrat négocié, ces prix sont faux et le scoring privilégie les mauvais types. Le NodeOverlay corrige la donnée d'entrée de la simulation, il ne change évidemment rien à votre facture. Deux champs mutuellement exclusifs existent : priceAdjustment applique un pourcentage ou un montant signé, price remplace la valeur par un prix absolu. Déclarer les deux fait rejeter la ressource.

apiVersion: karpenter.sh/v1alpha1
kind: NodeOverlay
metadata:
name: savings-plan-discount
spec:
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values: ["m5.large", "m5.xlarge", "m5.2xlarge"]
priceAdjustment: "-30%" # Réduction de 30% (Savings Plan)

Certaines ressources ne sont pas visibles par Karpenter au moment de la simulation, parce qu'elles n'apparaissent sur le nœud qu'après le démarrage d'un device plugin. Sans NodeOverlay, un pod qui demande smarter-devices/fuse reste Pending : Karpenter ne trouve aucun type d'instance capable de le satisfaire, puisqu'aucun n'annonce cette ressource. Le champ capacity ajoute uniquement des ressources étendues ; il ne permet pas de mentir sur cpu, memory, ephemeral-storage ou pods, qui sont explicitement refusés par la validation.

apiVersion: karpenter.sh/v1alpha1
kind: NodeOverlay
metadata:
name: custom-devices
spec:
requirements:
- key: node.kubernetes.io/instance-type
operator: In
values: ["g4dn.xlarge"]
capacity:
smarter-devices/fuse: "1"
custom-hardware/inference-unit: "4"

Le prix étant une valeur unique, deux overlays qui prétendent le modifier ne peuvent pas coexister : il faut départager. La capacité, elle, est une liste de ressources étendues, donc les contributions s'additionnent sans arbitrage. Retenez surtout la règle de départage à poids égal, contre-intuitive : c'est le nom qui vient le plus tard dans l'alphabet qui l'emporte. Nommez donc vos overlays avec un préfixe numérique si vous voulez un ordre lisible.

  1. weight le plus élevé gagne (valeur entre 1 et 10000, absent équivaut à 0)
  2. À weight égal, le nom le plus tard dans l'ordre alphabétique gagne
  3. Les capacity sont fusionnées (pas de conflit)
ChampComportement
price / priceAdjustmentL'overlay avec le plus haut weight gagne
capacityFusionné entre tous les overlays
  1. Karpenter provisionne, kube-scheduler schedule, ne confondez pas les rôles
  2. NodePool = contraintes (types, arch, limits) ; NodeClass = config cloud (AMI, réseau, IAM)
  3. nodeClassRef complet : group, kind, name, les trois sont obligatoires
  4. Toujours définir des limits, sinon Karpenter peut créer des centaines de nœuds
  5. 4 contrôleurs : Provisioner, Disruption, Termination, Garbage Collection
  6. Le GC Controller est critique, une erreur dans List() supprime tous vos nœuds
  7. Disruption graceful (consolidation, drift) respecte les budgets ; forceful (expiration, interruption) non
  8. NodeOverlay (alpha) permet d'ajuster prix et capacités pour la simulation

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn