FinOps Kubernetes : réduire le compute de 40 %
Cet article reprend après le travail de rightsizing : il remplace les node pools statiques et le réglage du Cluster Autoscaler par le provisioning de nodes de Karpenter, juste-à-temps et par bin-packing, pour que le cluster provisionne la forme d'instance réellement nécessaire à chaque pod plutôt que de tout forcer dans une poignée de formes prédéfinies. Si le rightsizing n'est pas encore fait chez vous, commencez par le guide d'optimisation des coûts.
Ce que Karpenter fait différemment
Karpenter abandonne complètement l'abstraction de node group. À la place, il surveille les pods unschedulable, regarde ce dont ils ont réellement besoin, et choisit l'instance type et la taille les moins chers qui satisfont ces contraintes parmi l'ensemble des instance types éligibles, puis appelle directement l'API EC2 Fleet. Pas d'ASG, pas de node group par forme.
Un pod en attente qui demande 400m de CPU et 512Mi de mémoire reçoit une petite instance ; un pod qui demande 8 vCPU et 32 Go en reçoit une tout autre : dans le même NodePool, en quelques secondes, parce que Karpenter résout un problème de bin-packing contre les prix et la capacité EC2 en temps réel.
Cela change aussi la posture vis-à-vis du spot : il devient une position par défaut plutôt qu'un second node pool qu'il faut construire et surveiller à la main. Un seul NodePool peut exprimer « préférer le spot, retomber sur l'on-demand », Karpenter se chargeant de la diversification d'instance types qui rend le spot vraiment fiable : ce que ratent souvent les équipes sous Cluster Autoscaler avec des ASG spot gérés manuellement, qui s'épinglent à deux ou trois instance types et subissent bien plus d'interruptions que nécessaire.
Le NodePool et l'EC2NodeClass
Deux CRDs font le travail. EC2NodeClass décrit les détails spécifiques à AWS : AMI, subnets, security groups, rôle IAM. NodePool décrit les contraintes de scheduling et le comportement de disruption.
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
name: default
spec:
amiFamily: AL2023
role: "karpenter-node-role"
subnetSelectorTerms:
- tags:
karpenter.sh/discovery: "prod-cluster"
securityGroupSelectorTerms:
- tags:
karpenter.sh/discovery: "prod-cluster"
blockDeviceMappings:
- deviceName: /dev/xvda
ebs:
volumeSize: 50Gi
volumeType: gp3
deleteOnTermination: trueapiVersion: karpenter.sh/v1
kind: NodePool
metadata:
name: general-purpose
spec:
template:
spec:
requirements:
- key: karpenter.sh/capacity-type
operator: In
values: ["spot", "on-demand"]
- key: kubernetes.io/arch
operator: In
values: ["amd64"]
- key: karpenter.k8s.aws/instance-category
operator: In
values: ["c", "m", "r"]
- key: karpenter.k8s.aws/instance-generation
operator: Gt
values: ["4"]
nodeClassRef:
group: karpenter.k8s.aws
kind: EC2NodeClass
name: default
expireAfter: 336h
limits:
cpu: 1000
memory: 1000Gi
disruption:
consolidationPolicy: WhenEmptyOrUnderutilized
consolidateAfter: 1mcapacity-type: In [spot, on-demand] sans pondération supplémentaire indique à Karpenter de préférer l'option la moins chère parmi les instance types qui satisfont les besoins du pod, et de retomber automatiquement sur l'on-demand dès que la capacité spot n'est plus disponible, que ce soit au lancement ou après une interruption. expireAfter: 336h force le remplacement des nodes toutes les deux semaines même si rien d'autre ne le déclenche, ce qui plafonne le temps pendant lequel un node peut dériver silencieusement d'une AMI à jour et force un re-bin-packing régulier.
Les charges de travail rejoignent ce pool via nodeSelector ou un topologySpreadConstraint sur les labels que Karpenter assigne :
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
spec:
replicas: 6
template:
spec:
nodeSelector:
karpenter.sh/capacity-type: spot
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: checkout-api
containers:
- name: checkout-api
image: registry.internal/checkout-api:1.4.2
resources:
requests:
cpu: "500m"
memory: "1Gi"La consolidation : ce que le Cluster Autoscaler n'a jamais bien fait
Le Cluster Autoscaler scale down en évinçant un node une fois qu'il est complètement vide et qu'un délai de scale-down s'est écoulé ; il ne déplace jamais des pods pour créer un node vide, ni ne se demande si la flotte est mal packée. La boucle de consolidation de Karpenter fait les deux, en continu : elle simule si les pods en cours pourraient tenir sur moins de nodes ou des nodes moins chers, et si oui cordonne, draine et termine l'excédent, laissant le scheduler re-packer le reste sur une empreinte plus resserrée. Contrairement à la policy WhenEmpty toute simple, consolidationPolicy: WhenEmptyOrUnderutilized cible aussi les nodes sous-utilisés, la différence entre une flotte qui rétrécit après un pic de trafic et une flotte qui reste gonflée indéfiniment.
Sur un cluster mixte de 60 nodes, activer la consolidation WhenEmptyOrUnderutilized (à la place de WhenEmpty seul) a fait passer le nombre moyen de nodes de 60 à 41 en une semaine, sans aucun changement des requests des pods, uniquement par re-packing. La facture de compute est passée d'environ 46 000 $/mois à 31 500 $/mois.
La consolidation est disruptive par construction : tout ce qui ne tolère pas un reschedule imprévu doit le déclarer explicitement :
apiVersion: apps/v1
kind: Pod
metadata:
name: batch-report-job
annotations:
karpenter.sh/do-not-disrupt: "true"
spec:
containers:
- name: report
image: registry.internal/report-generator:2.1.0karpenter.sh/do-not-disrupt bloque à la fois la consolidation et l'expiration sur le node qui héberge ce pod. C'est le bon outil pour un job stateful en plein checkpoint, mais je l'ai déjà vu laissé par copier-coller sur des Deployments de longue durée, épinglant silencieusement des nodes par ailleurs inoccupés. Il suffit de quelques annotations oubliées pour discrètement annuler les gains de consolidation vus plus haut.
La gestion des interruptions spot, nativement
L'ancien pattern, Cluster Autoscaler plus des ASG spot gérés manuellement, s'appuyait sur aws-node-termination-handler tournant en DaemonSet, qui interroge l'instance metadata service pour détecter le préavis d'interruption de deux minutes, puis cordonne et drain. Ça fonctionne, mais c'est un composant séparé à déployer, mettre à jour et surveiller, et il réagit après qu'AWS a déjà décidé de récupérer l'instance.
Karpenter surveille les mêmes signaux d'interruption, recommandations de rebalance et préavis de deux minutes, via une file SQS alimentée par des règles EventBridge qu'il gère lui-même. Dès qu'il reçoit un signal, il commence immédiatement à drainer le node et, à l'étape que les configurations manuelles sautent généralement, démarre le provisioning du remplacement avant que l'ancien node ne soit terminé, plutôt que d'attendre que le trou soit remarqué au prochain passage du Cluster Autoscaler.
Ce que cela donne dans les chiffres FinOps
Le rightsizing et le changement de modèle de provisioning se cumulent, ils ne se substituent pas l'un à l'autre : des requests réalistes donnent à Karpenter des entrées de bin-packing précises, et Karpenter transforme ces entrées en réduction réelle du nombre de nodes, au lieu de laisser du slack dans des groupes statiques surdimensionnés. Sur des missions où le rightsizing des requests était déjà fait mais où le cluster tournait encore sous Cluster Autoscaler avec deux ou trois node pools statiques, le seul passage à Karpenter a typiquement enlevé 30 à 40 % de plus sur la ligne compute, presque entièrement grâce à la consolidation et à une meilleure diversification spot.
Adopter tout ça ne touche à aucun dépôt applicatif. C'est un changement de plan de contrôle : un nouveau composant d'autoscaling, des définitions NodePool/EC2NodeClass, des PodDisruptionBudgets qui veulent vraiment dire ce qu'ils disent, et la consolidation qu'on laisse tourner. Le vrai coût apparaît après, côté ownership : quelqu'un maintient désormais la config NodePool comme on possédait autrefois des ASG, et quelqu'un audite l'usage de do-not-disrupt avant que ça ne devienne une planque pour de la capacité inoccupée.
Envie de faire tourner ça en production ?
Ce tutoriel couvre les concepts et l'architecture. Si vous voulez l'implémenter dans votre propre infrastructure, ou monter en compétence pour posséder ce sujet durablement, je propose du mentorat individuel construit autour de votre environnement réel, pas une formation générique.
Ce tutoriel
- Concepts clés et architecture principale
- Extraits de code illustratifs
- Le raisonnement derrière chaque décision
Mentorat individuel
- Des sessions de travail sur votre propre environnement
- Des réponses directes aux cas particuliers que vous rencontrez
- Un retour sur votre implémentation réelle
- Un accompagnement continu pendant que vous la construisez
Tutoriels similaires
Sécuriser un cluster AWS EKS de production
Les trois contrôles qui distinguent un cluster EKS durci d'un cluster par défaut : IRSA, NetworkPolicies default-deny, et contraintes OPA Gatekeeper.
Réduire vos coûts Kubernetes de 30 à 50 %
Un cadre pratique pour redimensionner requests/limits, ajuster VPA/HPA, adopter des node pools spot, et faire économiser le Cluster Autoscaler.
Le guide 2026 d'optimisation des coûts Cloud
Un cadre éprouvé pour réduire les dépenses cloud sans sacrifier la fiabilité : dimensionnement, remises d'engagement, et ce qui fait durer les économies.