Karpenter op EKS: snellere node-autoscaling met NodePool en EC2NodeClass

Karpenter provisiont nodes in 45–60 seconden op EKS door rechtstreeks de EC2 Fleet API aan te roepen in plaats van te wachten op Auto Scaling Groups. Waar Cluster Autoscaler uit voorgedefinieerde node groups kiest, evalueert Karpenter alle beschikbare instance types per batch van pending pods en lanceert de best passende node. Deze guide behandelt het installeren van Karpenter v1.x op EKS, het schrijven van NodePool- en EC2NodeClass-manifests, disruption en consolidation configureren, migreren vanaf Cluster Autoscaler zonder downtime, en monitoring via Prometheus.

Inhoudsopgave

Wat je hebt aan het einde

Een draaiende Karpenter-installatie op EKS die nodes provisiont op basis van daadwerkelijke pod-requirements, onderbenutte capaciteit automatisch consolideert, Spot-interruptions afhandelt, en metrics naar je Prometheus-monitoringstack stuurt.

Vereisten

  • Een EKS-cluster met Kubernetes 1.28+
  • kubectl, helm en aws CLI lokaal geinstalleerd
  • IAM-permissies om rollen, policies en EC2-tags aan te maken
  • Subnets en security groups getagd voor Karpenter-discovery (komt aan bod bij de installatiestappen)
  • Prometheus en Grafana geinstalleerd als je de monitoringsectie meteen werkend wilt hebben

Waarom Karpenter in plaats van Cluster Autoscaler

Cluster Autoscaler (CA) scant periodiek op pending pods en vraagt vervolgens een Auto Scaling Group om een node toe te voegen uit een voorgedefinieerde set instance types. Die round-trip duurt 3–5 minuten op een goede dag.

Karpenter slaat de ASG gewoon over. Het bekijkt elke unschedulable pod event-by-event, berekent welk instance type het best past uit maximaal 60 kandidaten, en roept de EC2 Fleet API direct aan. Het resultaat: nodes ready in 45–60 seconden.

Dimensie Cluster Autoscaler Karpenter
Trigger Periodieke scan (10+ s) Event per pending pod
Nodeselectie Voorgedefinieerde node groups Alle instance types die aan NodePool voldoen
Provisioningtijd 3–5 min (ASG-vertraging) 45–60 s (EC2 Fleet direct)
Consolidation Verwijdert alleen idle nodes Empty, multi-node en single-node consolidation
Instance-flexibiliteit Beperkt tot node group types Elk instance dat aan requirements voldoet

Teams die van CA naar Karpenter overstappen rapporteren doorgaans 20–40% kostenbesparing door betere bin-packing en automatische consolidation.

Even een belangrijk punt dat vaak verwarring geeft: Karpenter vervangt Cluster Autoscaler, niet HPA of VPA. HPA schaalt pods, VPA past resource requests aan, Karpenter provisiont de nodes waar die pods op draaien. Ze zijn complementaire lagen, geen alternatieven.

Karpenter installeren op EKS

Stap 1: omgevingsvariabelen instellen

export KARPENTER_NAMESPACE="kube-system"
export KARPENTER_VERSION="1.11.1"       # laatste stabiele versie per april 2026
export K8S_VERSION="1.31"               # moet overeenkomen met je EKS-clusterversie
export CLUSTER_NAME="production-main"
export AWS_DEFAULT_REGION="eu-west-1"
export AWS_ACCOUNT_ID="$(aws sts get-caller-identity --query Account --output text)"

Stap 2: IAM-rollen aanmaken

Karpenter heeft twee IAM-rollen nodig:

  1. KarpenterNodeRole voor de EC2-instances die het lanceert. Koppel AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryReadOnly en AmazonSSMManagedInstanceCore.
  2. KarpenterControllerRole voor de controller-pod zelf, via IRSA. Deze rol heeft scoped EC2-permissies nodig: ec2:RunInstances, ec2:CreateFleet, ec2:TerminateInstances, ec2:DescribeInstances, ec2:DescribeSubnets, ec2:DescribeSecurityGroups, ec2:DescribePlacementGroups (verplicht sinds v1.11), ec2:CreateTags, ec2:DeleteTags, iam:PassRole, iam:ListInstanceProfiles, ssm:GetParameter, sqs:ReceiveMessage, sqs:DeleteMessage, en meer.

Beveiligingsopmerking: elke principal die de tags karpenter.sh/managed-by, karpenter.sh/nodepool en kubernetes.io/cluster/${CLUSTER_NAME} kan aanmaken of verwijderen, kan indirect beinvloeden wat Karpenter provisiont. Beperk tag-CRUD in je IAM-policies.

Stap 3: subnets en security groups taggen

# Karpenter vindt subnets en security groups via tags
aws ec2 create-tags \
  --resources subnet-0abc1234 subnet-0def5678 sg-0aabb1122 \
  --tags Key=karpenter.sh/discovery,Value=${CLUSTER_NAME}

Karpenter kiest het subnet met de meeste beschikbare IP's per availability zone.

Stap 4: installeren met Helm

helm registry logout public.ecr.aws   # oude tokens opruimen

helm upgrade --install karpenter oci://public.ecr.aws/karpenter/karpenter \
  --version "${KARPENTER_VERSION}" \
  --namespace "${KARPENTER_NAMESPACE}" \
  --set "settings.clusterName=${CLUSTER_NAME}" \
  --set "settings.interruptionQueue=${CLUSTER_NAME}" \
  --wait

Stap 5: controleren of de controller draait

kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter --tail=20

Verwachte output bevat regels als controller started en watching for pending pods. Geen ERROR-regels over IAM of STS.

Een NodePool en EC2NodeClass aanmaken

Een NodePool definieert wat voor soort nodes Karpenter mag provisionen (instance families, capacity types, architecturen, limieten). Een EC2NodeClass definieert hoe ze op AWS worden aangemaakt (AMI, subnets, security groups, IAM, disk).

# ec2nodeclass.yaml
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: default
spec:
  role: "KarpenterNodeRole-production-main"

  amiSelectorTerms:
    - alias: "al2023@v20250301"  # pin in productie; gebruik nooit @latest

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: "production-main"

  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: "production-main"

  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 50Gi
        volumeType: gp3
        encrypted: true

  metadataOptions:
    httpEndpoint: enabled
    httpTokens: required          # alleen IMDSv2
    httpPutResponseHopLimit: 1
# nodepool.yaml
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        team: platform
    spec:
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
      requirements:
        - key: kubernetes.io/arch
          operator: In
          values: ["amd64"]
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand", "spot"]
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["m", "c", "r"]   # brede families voor bin-packing flexibiliteit
      expireAfter: 720h             # 30 dagen; dwingt node-refresh af
      terminationGracePeriod: 48h   # harde deadline voor draining
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m
    budgets:
      - nodes: "10%"
  limits:
    cpu: "1000"
    memory: "1000Gi"
  weight: 50

Beide toepassen:

kubectl apply -f ec2nodeclass.yaml -f nodepool.yaml

Controleer of Karpenter de NodePool ziet:

kubectl get nodepools

Verwachte output:

NAME      NODECLASS   WEIGHT   AGE
default   default     50       12s

Spot en on-demand met gewogen NodePools

Voor workloads die interruption tolereren, splits je op in twee NodePools met weight-based priority:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: spot
spec:
  weight: 100                    # hoger gewicht = eerst geprobeerd
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot"]
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["m", "c", "r", "m6i", "c6i", "r6i"]  # breed voor price-capacity-optimized
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: "500"
---
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: on-demand-fallback
spec:
  weight: 10
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["on-demand"]
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default
  limits:
    cpu: "500"

Karpenter probeert eerst de weight-100 Spot-pool. Als er geen Spot-capaciteit beschikbaar is, valt het terug op de weight-10 on-demand pool. Voor Spot gebruikt Karpenter de price-capacity-optimized allocation strategy, die prijs en interruptiekans afweegt in plaats van blindelings de goedkoopste pool te kiezen. Houd instance families breed: minder dan 15 instance types blokkeert single-node Spot-consolidation.

Voor GPU-workloads gebruik je een aparte NodePool met taints om dure GPU-nodes te isoleren van gewone workloads.

Disruption, consolidation en drift

Het disruptionmodel van Karpenter kent twee categorieen:

Voluntary (beperkt door disruption budgets):

  • Consolidation werkt in drie lagen: verwijder eerst lege nodes, probeer dan multi-node consolidation (verplaats workloads van meerdere nodes naar minder), en dan single-node consolidation (vervang een node door een kleinere). WhenEmptyOrUnderutilized schakelt alle drie in. WhenEmpty doet alleen lege nodes.
  • Drift detecteert wanneer een draaiende node niet meer overeenkomt met de gewenste NodePool- of EC2NodeClass-spec (gewijzigde AMI, aangepaste requirements, gewijzigde security groups). Karpenter vervangt gedrifte nodes geleidelijk.

Forceful (niet beperkt):

  • Expiration draineert en termineert nodes wanneer expireAfter verloopt (standaard 720h / 30 dagen).
  • Interruption handelt EC2-lifecycle-events af: Spot 2-minutenwaarschuwingen, gepland onderhoud, instance-stopsignalen. Karpenter provisiont alvast een vervanging tijdens het waarschuwingsvenster.

Disruption budgets per reden

Sinds v1.0 kun je budgets per disruptiereden scopen:

disruption:
  budgets:
    - nodes: "20%"
      reasons: ["Drifted"]
    - nodes: "10%"
      reasons: ["Underutilized"]
    - nodes: "0"
      reasons: ["Empty"]
      schedule: "0 9 * * mon-fri"   # blokkeer verwijdering lege nodes tijdens kantooruren
      duration: 8h

Specifieke pods beschermen

Voeg karpenter.sh/do-not-disrupt: "true" toe als pod-annotatie om voluntary disruption (consolidation, drift) op de node van die pod te blokkeren. Dit blokkeert geen expiration of Spot-interruption. Combineer het met PodDisruptionBudgets voor bredere beschikbaarheidsgaranties.

Migreren vanaf Cluster Autoscaler

Karpenter en Cluster Autoscaler kunnen naast elkaar draaien. Zero-downtime migratie volgt deze volgorde:

Stap 1: workloads voorbereiden

Voeg PodDisruptionBudgets toe aan elke productie-Deployment. Zonder PDB's worden bij het afschalen van oude node groups alle replica's in een keer ge-evict. Stel accurate resource requests in zodat Karpenter effectief kan bin-packen.

Stap 2: Karpenter deployen naast CA

Installeer Karpenter zoals hierboven beschreven. Gebruik nodeAffinity op het Karpenter-controller Deployment om het vast te pinnen op nodes in je bestaande managed node group. Karpenter mag niet draaien op nodes die het zelf beheert (circulaire dependency als het zijn eigen controller evict).

Stap 3: NodePool en EC2NodeClass aanmaken

Pas de manifests uit de vorige secties toe. Karpenter begint direct met het bekijken van unschedulable pods, maar raakt bestaande CA-managed nodes niet aan.

Stap 4: Cluster Autoscaler naar nul schalen

kubectl scale deployment cluster-autoscaler -n kube-system --replicas=0

Stap 5: node group-capaciteit geleidelijk verlagen

Verlaag de minSize en desiredCapacity van je ASG's stapsgewijs. Naarmate workloads op natuurlijke wijze churnen (deployments, scaling events, pod-restarts), landen pods op Karpenter-geprovisionneerde nodes. Oude nodes draineen door normale turnover.

aws autoscaling update-auto-scaling-group \
  --auto-scaling-group-name eks-managed-ng-1 \
  --min-size 2 \
  --desired-capacity 2

Houd minimaal 2 nodes per AZ in de oorspronkelijke node group totdat je hebt bevestigd dat Karpenter alle workloads afhandelt.

Stap 6: verifieren en opruimen

kubectl get nodes -L karpenter.sh/nodepool

Nodes met een karpenter.sh/nodepool-label worden door Karpenter beheerd. Zodra er geen workloads meer op onbeheerde nodes draaien, kun je de oude ASG's verwijderen.

Salesforce migreerde meer dan 1.000 EKS-clusters naar Karpenter met precies deze gefaseerde aanpak.

Karpenter monitoren met Prometheus en Grafana

Karpenter stelt Prometheus-metrics beschikbaar op karpenter.kube-system.svc.cluster.local:8080/metrics. Als je kube-prometheus-stack draait, voeg dan een ServiceMonitor toe:

apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: karpenter
  namespace: kube-system
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: karpenter
  endpoints:
    - port: http-metrics
      path: /metrics

Belangrijke metrics om te volgen

Metric Wat het je vertelt
karpenter_nodes_created_total Hoeveel nodes Karpenter heeft geprovisioned
karpenter_nodes_terminated_total Hoeveel nodes zijn verwijderd (consolidation, expiry, interruption)
karpenter_pods_startup_duration_seconds Tijd van pod-creatie tot running state
karpenter_scheduler_queue_depth Pending pod-batches die op nodes wachten
karpenter_voluntary_disruption_decisions_total Consolidation- en drift-beslissingen
karpenter_nodes_termination_duration_seconds Draintijd; hoge p95 signaleert vastgelopen PDB's
karpenter_voluntary_disruption_eligible_nodes Nodes die in aanmerking komen voor consolidation maar niet worden aangepakt

Volledige referentie: Karpenter metrics-documentatie.

Grafana-dashboards

Importeer deze van Grafana Labs:

  • Karpenter Overview (ID 21699) voor NodePool-, node- en pod-aantallen
  • Karpenter Performance (ID 22173) voor cloud provider-fouten en pod-opstarttijd
  • Karpenter Activity (ID 18862) voor scale-up/down event-tijdlijnen

Alerts die de moeite waard zijn

  • Langdurig hoge queue depth: karpenter_scheduler_queue_depth > 5 langer dan 2 minuten betekent dat Karpenter geen capaciteit kan vinden. Controleer NodePool-limieten, instance-beschikbaarheid en IAM-permissies.
  • Trage termination: histogram_quantile(0.95, karpenter_nodes_termination_duration_seconds_bucket) > 600 betekent dat draining langer dan 10 minuten duurt. Kijk naar blokkerende PDB's of do-not-disrupt-annotaties.
  • Provisioning-piek: een plotselinge stijging in rate(karpenter_nodeclaims_created_total[5m]) kan wijzen op een kapotte HPA-loop of een losgeslagen Deployment.
  • Consolidation geblokkeerd: karpenter_voluntary_disruption_eligible_nodes blijft hoog terwijl karpenter_voluntary_disruption_decisions_total vlak blijft. Disruption budgets of PDB's voorkomen opruiming.

Production hardening checklist

  • Pin AMI-versies. Gebruik al2023@v20250301, niet @latest. Test AMI-updates in staging voordat je ze via drift uitrolt.
  • Draai Karpenter op Fargate of een dedicated managed node group. Nooit op Karpenter-managed nodes. Een circulaire dependency betekent dat Karpenter zijn eigen controller evict.
  • Vereis IMDSv2. Stel httpTokens: required in bij EC2NodeClass metadataOptions om SSRF-gebaseerde credential-diefstal te blokkeren.
  • Stel NodePool resource-limieten in. Definieer altijd limits.cpu en limits.memory om uitgaven per NodePool te begrenzen.
  • Gebruik IRSA voor de controllerrol. Koppel IAM-permissies nooit via EC2-instance metadata.
  • Houd instance families breed voor Spot. Minder dan 15 instance type-opties blokkeert single-node Spot-consolidation.
  • Stel terminationGracePeriod in wanneer je expireAfter gebruikt. Zonder dat blokkeert een pod met do-not-disrupt de node-drain oneindig.

Veelvoorkomende valkuilen

Pods blijven in Pending ondanks beschikbare NodePools. De pod-requirements (resource requests, node selectors, tolerations) passen niet binnen de requirements van een NodePool. Voer kubectl describe pod <naam> uit en bekijk de Events-sectie voor scheduling-faalredenen. Karpenter kan alleen nodes provisionen die voldoen aan de doorsnede van NodePool-constraints en pod-constraints.

Nodes worden aangemaakt en meteen weer getermineerd. De EC2-instance start maar slaagt er niet in om het cluster te joinen. Veelvoorkomende oorzaken: ontbrekende VPC-endpoints voor STS of SSM in private clusters, verkeerde security group-regels die kubelet-communicatie blokkeren, of een verkeerd IAM-instance profile.

Consolidation vindt niet plaats. Check karpenter_voluntary_disruption_eligible_nodes. Als die hoog is maar decisions nul, blokkeren disruption budgets of PDB's. Controleer ook of consolidationPolicy op WhenEmptyOrUnderutilized staat, niet op WhenEmpty.

Trage Windows-nodes. Windows-nodes doen er ~6 minuten over om het cluster te joinen plus 15–20 minuten om de base image te pullen. Dit is een inherente platformbeperking, geen Karpenter-probleem. Verwacht geen sub-minuut provisioning voor Windows-workloads.

v1.0-migratiefouten. Als je upgradet van Karpenter v0.x, zijn de Provisioner- en AWSNodeTemplate-CRD's verwijderd in v1.0. Draai karpenter-convert -f provisioner.yaml > nodepool.yaml voordat je upgradet. v1.1 dropt v1beta1 volledig.

Wanneer escaleren

Verzamel deze informatie voordat je hulp vraagt:

  • Karpenter-controllerlogs: kubectl logs -n kube-system -l app.kubernetes.io/name=karpenter --tail=100
  • NodePool- en EC2NodeClass-specs: kubectl get nodepools -o yaml en kubectl get ec2nodeclasses -o yaml
  • Pending pods en hun events: kubectl get pods --field-selector=status.phase=Pending -A
  • NodeClaim-status: kubectl get nodeclaims -o wide
  • Karpenter-versie: helm list -n kube-system | grep karpenter
  • EKS-clusterversie en platformversie
  • IAM-rol ARN's voor zowel controller- als noderollen

Terugkerende server- of deploymentproblemen?

Ik help teams productie betrouwbaar maken met CI/CD, Kubernetes en cloud—zodat fixes blijven en deploys geen stress meer zijn.

Bekijk DevOps consultancy

Doorzoek deze site

Begin met typen om te zoeken, of blader door de kennisbank en blog.