Chez ENGIE, j’ai mené la migration du Cluster Autoscaler vers Karpenter sur des clusters EKS de production — six clusters qui font tourner une plateforme DevOps servant plus de 600 organisations. Pas un lab, pas un POC : des clusters avec des builds CI qui arrivent par vagues et des équipes qui remarquent la moindre latence de scheduling.
Voici ce que je referais à l’identique, et ce que j’aurais aimé savoir avant de commencer.
Pourquoi quitter le Cluster Autoscaler
Le Cluster Autoscaler a bien servi. Mais sur EKS, il traîne trois limites structurelles.
Il ne parle pas à EC2, il parle aux Auto Scaling Groups
Le Cluster Autoscaler ne crée pas de nœuds : il ajuste le desiredCount d’un ASG, puis attend. Décision, appel ASG, lancement d’instance, bootstrap, enregistrement du nœud — chaque couche ajoute son délai. Pour des runners CI éphémères qui arrivent par rafales, ces minutes d’attente se paient en pipelines qui patientent.
Un node group par forme de nœud
Un ASG est homogène : un type d’instance (ou une famille proche), une zone de décision. Pour couvrir des besoins variés — du build ARM, du gros consommateur mémoire, du spot — on empile les node groups. On finit avec une matrice de groupes à maintenir dans Terraform, et des nœuds taillés « au cas où » plutôt qu’au besoin réel.
Le scale-down est timide
Le Cluster Autoscaler retire les nœuds vides, prudemment. Mais il ne sait pas dire : « ces trois nœuds à moitié pleins tiendraient sur un seul, plus petit, moins cher ». Cette décision-là — le repacking — c’est précisément ce qui manque pour faire du FinOps sérieux sur un cluster.
Karpenter en deux objets
Karpenter inverse le modèle : plus d’ASG. Le contrôleur observe les pods en attente, calcule la forme de nœud optimale et appelle directement l’API EC2. Toute la configuration tient dans deux ressources.
NodePool : ce que le cluster a le droit de provisionner
Le NodePool définit l’enveloppe : familles d’instances autorisées, architectures, capacité spot ou à la demande, limites globales, et la politique de disruption (consolidation, expiration des nœuds).
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: default
spec:
template:
spec:
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["on-demand", "spot"]
- key: kubernetes.io/arch
operator: In
values: ["amd64", "arm64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
# Renouvellement des nœuds : AMI fraîches, dérive limitée
expireAfter: 720h
limits:
cpu: "200"
memory: 800Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 5m
budgets:
# Jamais plus de 10 % des nœuds en mouvement
- nodes: "10%"
# Zéro disruption volontaire pendant les heures ouvrées
- nodes: "0"
schedule: "0 8 * * mon-fri"
duration: 10hNotez les budgets : c’est eux qui rendent la consolidation vivable en production. On y revient plus bas.
EC2NodeClass : comment les nœuds sont construits
L’EC2NodeClass porte le côté AWS : AMI, rôle IAM, sous-réseaux, security groups. La découverte par tags évite de coder en dur des identifiants de ressources.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiSelectorTerms:
# En production : épingler une version validée (al2023@vYYYYMMDD),
# jamais @latest — une nouvelle AMI déclenche le remplacement des nœuds.
- alias: al2023@latest
role: KarpenterNodeRole-prod-cluster
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: prod-cluster
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: prod-cluster
tags:
team: platform
managed-by: karpenterDeux objets, versionnés dans Git, déployés par ArgoCD comme le reste. C’est toute la configuration — comparez avec la pile de node groups Terraform qu’ils remplacent.
Une migration progressive, cluster par cluster
Le bon réflexe : ne jamais basculer d’un coup. Karpenter et le Cluster Autoscaler cohabitent très bien, à une condition — qu’ils ne se disputent pas les mêmes pods. Ma séquence, validée cluster après cluster en commençant par le moins critique :
- Préparer le terrain : rôle IAM des nœuds, file SQS d’interruption spot, tags
karpenter.sh/discoverysur les sous-réseaux et security groups. - Installer Karpenter sur un node group statique dédié (ou Fargate) — jamais sur la capacité qu’il gère lui-même.
- Déployer
NodePooletEC2NodeClass, puis valider sur une charge de test que les nœuds montent et redescendent proprement. - Couper le Cluster Autoscaler sans le désinstaller : un
scale --replicas=0se rétablit en dix secondes si besoin. - Réduire les anciens node groups par paliers et drainer les nœuds un par un : chaque pod expulsé devient un pod en attente, que Karpenter replace sur un nœud taillé juste.
# Karpenter est en place et observe
kubectl get nodepools,ec2nodeclasses
# Couper le Cluster Autoscaler — réversible à tout instant
kubectl -n kube-system scale deployment cluster-autoscaler --replicas=0
# Réduire le node group historique par paliers
eksctl scale nodegroup --cluster prod-cluster --name workers-legacy \
--nodes 4 --nodes-min 4
# Drainer les anciens nœuds, un par un, en heures creuses
kubectl cordon ip-10-0-12-34.eu-west-1.compute.internal
kubectl drain ip-10-0-12-34.eu-west-1.compute.internal \
--ignore-daemonsets --delete-emptydir-data
# Suivre les décisions de Karpenter en direct
kubectl get nodeclaims -w
kubectl -n kube-system logs -l app.kubernetes.io/name=karpenter --tail=50À chaque palier, je laisse tourner un ou deux jours. Le rollback reste trivial tant que les anciens node groups existent : remonter le Cluster Autoscaler, limiter le NodePool à zéro. C’est ce filet qui rend la migration sereine.
Les pièges que j’ai vraiment rencontrés
Les PodDisruptionBudgets trop stricts
Karpenter respecte les PDB — c’est sa qualité, et votre premier piège. Un PDB avec maxUnavailable: 0, ou un minAvailable égal au nombre de réplicas, rend le drain impossible : le nœud devient inamovible, la consolidation et l’expiration se bloquent dessus, silencieusement. Avant de migrer, auditez tous les PDB du cluster (kubectl get pdb -A) et corrigez ceux qui interdisent tout mouvement. Un PDB doit protéger la disponibilité, pas figer l’infrastructure.
Les DaemonSets comptent dans l’addition
Karpenter intègre les requests des DaemonSets dans le calcul de la taille des nœuds — encore faut-il que ces requests existent. Un agent d’observabilité sans requests déclarées fausse tout le bin-packing. Et si votre stack de collecte est riche (la nôtre l’est : Alloy, Vector…), les petites instances deviennent contre-productives : le DaemonSet y consomme une part disproportionnée du nœud. Excluez les petites tailles via les requirements du NodePool.
La consolidation, à apprivoiser
WhenEmptyOrUnderutilized est l’option qui rapporte — et celle qui bouscule. Karpenter remplace activement des nœuds sous-utilisés, donc déplace des pods, en continu. Trois garde-fous indispensables : des budgets de disruption (dont une fenêtre à zéro en heures ouvrées, comme dans le YAML plus haut), un consolidateAfter qui laisse retomber les pics, et l’annotation karpenter.sh/do-not-disrupt: "true" sur les pods qui ne doivent jamais être déplacés — builds CI en cours, jobs longs, workloads à état.
L’œuf et la poule du contrôleur
Karpenter ne peut pas tourner sur des nœuds qu’il provisionne : s’il consolide son propre nœud, plus personne ne pilote. Gardez un petit node group statique (ou Fargate) pour le contrôleur, CoreDNS et les briques critiques. C’est le seul ASG qui survit à la migration — et c’est très bien comme ça.
Le spot sans file d’interruption
Si vous activez le spot, configurez la file SQS d’interruption (settings.interruptionQueue). Sans elle, Karpenter découvre la récupération de l’instance en même temps que vos pods. Avec elle, il draine proactivement dès l’avis EC2 — deux minutes qui changent tout.
Ce que vous pouvez en attendre
Je me méfie des chiffres recyclés de conférence, alors restons sur ce que la mécanique garantit :
- Des nœuds en secondes, pas en minutes : l’appel direct à EC2 supprime la couche ASG. Sur des charges CI en rafales, la différence se voit à l’œil nu dans les files d’attente.
- Des nœuds taillés au besoin réel : Karpenter choisit le type d’instance par calcul, pod par pod, au lieu de piocher dans des formats prédécidés. Le gâchis structurel disparaît.
- Un levier FinOps actif : la consolidation repacke le cluster en continu, et l’accès au spot devient une ligne de YAML, diversification des pools comprise.
- Moins d’infrastructure à maintenir : des dizaines de node groups Terraform remplacés par deux ressources Kubernetes, gérées en GitOps comme le reste.
TODO(Khalil): ajouter ici les chiffres réels avant/après de la migration (temps de provisioning, coût compute mensuel, nombre de node groups supprimés) extraits de Grafana, après accord d’ENGIE sur leur publication.
Si vous préparez cette migration et que vous voulez en parler — PDB récalcitrants, stratégie spot, ordre des clusters — écrivez-moi. C’est exactement le genre de chantier que j’aime cadrer.