Réduire vos coûts Kubernetes sur GKE
Cet article couvre les trois niveaux où les clusters Kubernetes gaspillent du compute (les requests/limits des pods, la composition du node pool et le réglage du cluster autoscaler), et explique pourquoi les grosses économies viennent de la correction des trois ensemble. Les exemples ciblent GKE Standard. Si vous êtes sur EKS, la suite consacrée à Karpenter couvre le côté AWS.
Où les clusters Kubernetes saignent réellement de l'argent
Il y a trois niveaux indépendants, et la plupart des équipes n'en ont jamais ajusté qu'un seul (généralement les types d'instance des nodes) :
Niveau 1 : Requests/limits des pods → Demandez-vous plus que ce que vous utilisez ?
Niveau 2 : Composition du node pool → Payez-vous le tarif à la demande pour des workloads qui pourraient tourner en spot ?
Niveau 3 : Cluster Autoscaler → Le cluster réduit-il réellement sa taille quand la charge baisse ?Corriger le Niveau 2 sans le Niveau 1 revient à surprovisionner sur des nodes moins chers. On économise vraiment, mais seulement une partie de ce qui est possible. Les fortes réductions viennent de la correction des trois niveaux ensemble, parce que chacun multiplie l'effet des autres.
Niveau 1 : redimensionner requests et limits
Récupérez au moins 7 à 14 jours d'utilisation réelle par workload avant de toucher quoi que ce soit, car une seule journée chargée donne des chiffres trompeurs. Si vous faites tourner metrics-server (ce qui devrait être le cas) plus Prometheus, faites une vérification rapide avec kubectl top, puis comparez dans le temps l'usage réel à ce que chaque conteneur demande. container_cpu_usage_seconds_total est un compteur, il faut donc toujours l'encapsuler dans rate() :
# Cœurs CPU réellement utilisés, par conteneur (tracez le p95 sur 7 à 14 jours)
sum by (namespace, pod, container) (rate(container_cpu_usage_seconds_total{container!=""}[5m]))
# Mémoire réellement utilisée, par conteneur
max by (namespace, pod, container) (container_memory_working_set_bytes{container!=""})Déployez le Vertical Pod Autoscaler en mode recommandation seule. Évitez les modes basés sur l'éviction lors d'un premier passage. Ils redémarrent les pods, et vous voulez voir les chiffres avant que quoi que ce soit ne redémarre en production :
apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
name: checkout-api-vpa
namespace: payments
spec:
targetRef:
apiVersion: "apps/v1"
kind: Deployment
name: checkout-api
updatePolicy:
updateMode: "Off" # recommandation seule, rien n'est évincé automatiquement
resourcePolicy:
containerPolicies:
- containerName: "*"
minAllowed:
cpu: 50m
memory: 64Mi
maxAllowed:
cpu: 2
memory: 2GiSur GKE, le VPA est intégré. Activez-le sur le cluster (vertical_pod_autoscaling { enabled = true } sur le google_container_cluster) plutôt que d'installer les composants upstream.
Définissez des requests CPU, mais pas de limits CPU. Le CPU est compressible. Une limite CPU stricte throttle votre application sous charge même quand le node a des cycles libres, sans rien libérer, puisque la request garantit déjà au conteneur sa part. Pour la mémoire, c'est différent, car un pod OOMKilled est un vrai incident. Définissez donc requests et limits mémoire, et gardez une marge de sécurité sur la limite, même après redimensionnement des requests.
Après une à deux semaines de recommandations VPA (kubectl describe vpa checkout-api-vpa -n payments affiche la Target par conteneur), appliquez-les comme requests statiques dans vos manifestes, ou passez en updateMode: "Recreate" une fois les chiffres validés (InPlaceOrRecreate si votre version du VPA gère le redimensionnement à chaud ; les anciennes versions appellent ce mode par éviction Auto, désormais déprécié). Ne laissez pas le VPA et un HPA agir tous les deux sur le CPU d'un même workload, ils se contredisent. D'expérience, cette étape est généralement la plus rentable des trois. Il est courant de trouver des requests CPU réglées à plusieurs fois l'usage p95 observé.
Niveau 2 : node pools Spot
Tous les workloads n'ont pas leur place en spot, segmentez par tolérance à la disruption, pas par équipe.
- Services stateless, à scaling horizontal, derrière un load balancer → Spot, par défaut.
- Tout ce qui a un état local, les jobs batch longs sans checkpointing, ou tout ce qui tourne en réplique unique → à la demande.
resource "google_container_node_pool" "spot_workers" {
name = "spot-workers"
cluster = google_container_cluster.primary.name
location = var.region # pool régional : nodes répartis sur les zones de la région
autoscaling {
# les limites total_* s'appliquent au pool entier ; min_node_count/max_node_count
# seraient *par zone* sur un pool régional (20 par zone = 60 nodes sur 3 zones)
total_min_node_count = 0
total_max_node_count = 20
location_policy = "ANY" # prendre la capacité Spot dans la zone qui en a
}
node_config {
machine_type = "e2-standard-4"
spot = true
labels = {
workload-class = "spot-tolerant"
}
taint {
key = "cloud.google.com/gke-spot"
value = "true"
effect = "NO_SCHEDULE"
}
}
}GKE applique automatiquement le label cloud.google.com/gke-spot: "true" aux nodes Spot. Associez le taint à une toleration correspondante et à un nodeSelector dans le template de pod du workload :
spec:
nodeSelector:
cloud.google.com/gke-spot: "true"
tolerations:
- key: cloud.google.com/gke-spot
operator: Equal
value: "true"
effect: NoScheduleEt définissez un PodDisruptionBudget pour qu'un drain ou un scale-down ne puisse pas faire tomber toutes les répliques en même temps :
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: checkout-api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: checkout-apiLe PDB protège contre les évictions volontaires (drains, scale-down de l'autoscaler). Une préemption Spot n'est pas une éviction, elle ne tient donc pas compte du PDB. Ce qui vous protège de la préemption, c'est d'avoir assez de répliques réparties sur plusieurs zones, et location_policy ainsi qu'une topologySpreadConstraint sur la zone y contribuent. Google annonce des remises de l'ordre de 60 à 91 % sur les VM Spot par rapport au tarif à la demande. Le chiffre réel varie selon le type de machine et la région, vérifiez donc la tarification actuelle pour les vôtres.
Niveau 3 : faire redescendre le cluster autoscaler
Le problème le plus courant, c'est un autoscaler qui monte sans souci mais ne redescend jamais, parce que quelque chose sur le node ne peut pas être déplacé. Sur GKE, les blocages habituels sont :
- un pod sans contrôleur (un
Podbrut, pas unDeployment/StatefulSet/Job) ; - un pod
kube-systemqui n'est pas un DaemonSet et n'a pas dePodDisruptionBudget(typiquement un add-on de métriques ou de DNS placé sur un node de travail) ; - un PDB qui n'autorise aucune disruption (
minAvailableégal au nombre de répliques) ; - un pod annoté
cluster-autoscaler.kubernetes.io/safe-to-evict: "false", ou dont l'affinité, le node selector ou les requests l'empêchent de tenir ailleurs.
Les pods DaemonSet ne bloquent pas le scale-down ; ils tournent de toute façon sur chaque node. Pour un pod que vous savez déplaçable sans risque, annotez son template avec cluster-autoscaler.kubernetes.io/safe-to-evict: "true", ou donnez un PDB aux workloads kube-system.
GKE fait tourner le cluster autoscaler pour vous et n'expose pas ses flags. Ce que vous pouvez changer, c'est le profil d'autoscaling du cluster :
resource "google_container_cluster" "primary" {
# ... reste de la configuration du cluster
cluster_autoscaling {
enabled = false # auto-provisioning des nodes désactivé ; l'autoscaling par pool s'applique toujours
autoscaling_profile = "OPTIMIZE_UTILIZATION"
}
}OPTIMIZE_UTILIZATION réduit la taille du cluster plus agressivement et privilégie le placement des pods sur des nodes déjà bien remplis. Le profil par défaut BALANCED garde plus de capacité libre pour que les pods soient placés plus vite. J'utiliserais OPTIMIZE_UTILIZATION pour les clusters batch et sensibles aux coûts. Pour des services sensibles à la latence avec un trafic en pics, surveillez le temps d'attente des pods en Pending après le changement.
Si vous faites tourner vous-même le Cluster Autoscaler open source (par exemple sur EKS), vous le réglez par flags, et deux comportements diffèrent de GKE. Les nodes qui hébergent des pods avec du stockage local, emptyDir compris, sont ignorés par défaut (--skip-nodes-with-local-storage=true ; exemptez un pod avec l'annotation cluster-autoscaler.kubernetes.io/safe-to-evict-local-volumes listant ses volumes), et les pods kube-system sans PDB bloquent le scale-down (--skip-nodes-with-system-pods=true). Les valeurs par défaut actuelles sont déjà --scale-down-utilization-threshold=0.5, --scale-down-unneeded-time=10m et --expander=least-waste, vérifiez donc ce que votre version utilise avant de les « régler ». --balance-similar-node-groups est désactivé par défaut et mérite d'être activé avec des node groups multi-zones.
HPA : éviter le piège du thrashing
Le Horizontal Pod Autoscaler réglé uniquement sur le %CPU brut a tendance à osciller sous un trafic en rafales, ça monte, puis redescend aussitôt, au risque de brefs trous de capacité. Commencez par une fenêtre de stabilisation et une cible d'utilisation réaliste : 70 %, pas 50 %.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: checkout-api-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: checkout-api
minReplicas: 3
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60Comment les trois niveaux se combinent
Les effets des niveaux se multiplient, donc en corriger un seul déçoit en général. Voici une façon approximative d'estimer votre plafond avant de faire le travail :
nouveau_coût_compute ≈ ancien_coût_compute
× (CPU total demandé après / CPU total demandé avant) # Niveaux 1 + 3
× (1 − part_spot × remise_spot) # Niveau 2Considérez le résultat comme une borne haute. Le bin-packing n'est jamais parfait, les DaemonSets et les réservations système ne diminuent pas, et tous les workloads ne peuvent pas passer en Spot. Mesurez le vrai avant/après avec l'allocation des coûts GKE ou votre export de facturation.
C'est le déroulé que j'applique dans une mission d'Audit des coûts Cloud, avec vos propres métriques plutôt qu'une formule.
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
FinOps Kubernetes sur EKS avec Karpenter
Remplacez les node groups statiques d'EKS par le provisioning just-in-time de Karpenter pour réduire le compute.
Le guide 2026 d'optimisation des coûts Cloud
Un cadre éprouvé pour réduire les dépenses cloud sans sacrifier la fiabilité, du dimensionnement aux remises d'engagement.