Command Palette

Search for a command to run...

Mission 0.3 · Stage 0 — Platform Foundations: The Paved Road on Kubernetes

Capacity on Demand: Karpenter and Cost-Aware Nodes

Goal: The cluster adds right-sized nodes (including Spot) within a minute when pods are pending, and consolidates them when load drops.

45 min Pay only for nodes Karpenter launches 5 steps 2 break-it drills

By the end of this mission

  • Explain how Karpenter differs from Cluster Autoscaler
  • Define NodePools and EC2NodeClasses with instance flexibility and Spot
  • Watch provisioning and consolidation happen
  • Protect workloads during consolidation with PDBs and annotations

Part 1

Understand it first

Karpenter vs Cluster Autoscaler

Cluster Autoscaler (Kubernetes course, cluster autoscaler topic) scales pre-defined node groups up and down: you guess instance types in advance, and it adds nodes of that type. KARPENTER looks at the PENDING pods' actual requirements (CPU, memory, architecture, zone, GPU, Spot tolerance) and launches whatever EC2 instance fits best, directly through the EC2 API, usually in under a minute. It also CONSOLIDATES: when nodes are underused, it moves pods and replaces or removes nodes to cut cost.

NodePool and EC2NodeClass

A NodePool states WHAT is allowed: instance categories and sizes, capacity types (spot/on-demand), architectures, zones, limits on total CPU, taints, and the disruption policy. An EC2NodeClass states HOW to launch on AWS: AMI family, subnets and security groups (found by tags that Terraform created), IAM role, disk. The platform team owns these; teams influence placement through normal pod requests, node selectors, and tolerations.

Spot, safely

Spot instances cost up to ~70–90% less but can be reclaimed with 2 minutes' notice. Karpenter handles interruption notices (via an SQS queue fed by EventBridge) by cordoning and draining the node early. Allow many instance types so Spot capacity is available, keep stateful and single-replica workloads on on-demand, and make sure stateless workloads have multiple replicas and PDBs.

Part 2

Your project after this mission · 2 files change

shoplite-platform/
  • shoplite-platform/
    • addons/
      • karpenter/
        • karpenter-app.yamlnew
        • nodepools.yamlnew

Part 3

Build it, step by step

  1. 1

    Prerequisites from Terraform

    Karpenter needs an IAM role for its controller (Pod Identity), a node IAM role, an SQS interruption queue with EventBridge rules, and subnets and security groups tagged karpenter.sh/discovery: shoplite-prod. The official Terraform module (terraform-aws-modules/eks/aws//modules/karpenter) creates all of it. Add it to the EKS stack from the AWS/Terraform courses and apply.

    terminal
    $ terraform -chdir=envs/prod apply -target=module.karpenter
    aws sqs list-queues --queue-name-prefix Karpenter
    ── expected output ──
    Apply complete! Resources: 11 added, 0 changed, 0 destroyed.
    { "QueueUrls": [ "https://sqs.ap-south-1.amazonaws.com/123456789012/Karpenter-shoplite-prod" ] }
  2. 2

    Install the controller via Argo CD

    Chart oci://public.ecr.aws/karpenter/karpenter. Run the controller itself on a small fixed managed node group (not on nodes it manages).

    shoplite-platform/addons/karpenter/karpenter-app.yamlwhole fileyaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: karpenter
      namespace: argocd
    spec:
      project: default
      source:
        repoURL: public.ecr.aws/karpenter
        chart: karpenter
        targetRevision: 1.7.1
        helm:
          valuesObject:
            settings:
              clusterName: shoplite-prod
              interruptionQueue: Karpenter-shoplite-prod
            nodeSelector: { role: system }
      destination:
        server: https://kubernetes.default.svc
        namespace: kube-system
      syncPolicy:
        automated: { prune: true, selfHeal: true }
  3. 3

    Define the node pools

    general allows Spot and on-demand across many instance families (flexibility is what makes Spot reliable) with a CPU limit as a cost guardrail. consolidationPolicy: WhenEmptyOrUnderutilized lets Karpenter repack pods. budgets limit how many nodes can be disrupted at once. A second on-demand pool with a taint is for stateful workloads that tolerate it.

    shoplite-platform/addons/karpenter/nodepools.yamlwhole fileyaml
    apiVersion: karpenter.k8s.aws/v1
    kind: EC2NodeClass
    metadata: { name: default }
    spec:
      amiSelectorTerms: [{ alias: al2023@latest }]
      role: KarpenterNodeRole-shoplite-prod
      subnetSelectorTerms: [{ tags: { karpenter.sh/discovery: shoplite-prod } }]
      securityGroupSelectorTerms: [{ tags: { karpenter.sh/discovery: shoplite-prod } }]
      blockDeviceMappings:
        - deviceName: /dev/xvda
          ebs: { volumeSize: 50Gi, volumeType: gp3, encrypted: true }
    ---
    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata: { name: general }
    spec:
      template:
        spec:
          nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: default }
          requirements:
            - { key: karpenter.sh/capacity-type, operator: In, values: [spot, on-demand] }
            - { 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: ["5"] }
          expireAfter: 720h          # recycle nodes monthly (fresh AMIs, patches)
      limits: { cpu: "200" }
      disruption:
        consolidationPolicy: WhenEmptyOrUnderutilized
        consolidateAfter: 1m
        budgets: [{ nodes: "10%" }]
    ---
    apiVersion: karpenter.sh/v1
    kind: NodePool
    metadata: { name: on-demand-stateful }
    spec:
      template:
        spec:
          nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: default }
          requirements:
            - { key: karpenter.sh/capacity-type, operator: In, values: [on-demand] }
          taints: [{ key: shoplite.dev/stateful, effect: NoSchedule }]
      disruption: { consolidationPolicy: WhenEmpty, consolidateAfter: 10m }
  4. 4

    Watch it provision and consolidate

    Scale a test deployment so pods go Pending, and watch a NodeClaim appear and become a Ready node. Scale down and watch consolidation remove it.

    terminal
    $ kubectl create deploy inflate --image=public.ecr.aws/eks-distro/kubernetes/pause:3.10 --replicas=0
    kubectl set resources deploy inflate --requests=cpu=1
    kubectl scale deploy inflate --replicas=20
    kubectl get nodeclaims -w
    ── expected output ──
    NAME TYPE CAPACITY ZONE NODE READY
    general-8x2kq c7g.4xlarge spot ap-south-1b Unknown
    general-8x2kq c7g.4xlarge spot ap-south-1b ip-10-20-12-87.ap-south-1.compute.internal True
  5. 5

    Scale down and see consolidation

    After consolidateAfter, Karpenter terminates the empty node. kubectl get events shows its reasoning.

    terminal
    $ kubectl scale deploy inflate --replicas=0
    kubectl get events -A --field-selector source=karpenter --sort-by=.lastTimestamp | tail -2
    ── expected output ──
    Normal DisruptionTerminating nodeclaim/general-8x2kq Disrupting NodeClaim: Empty/Delete
    Normal Finalized node/ip-10-20-12-87... Finalized karpenter.sh/termination

Checkpoint — you should now have

  • ✓Pending pods produce a right-sized node in about a minute.
  • ✓Empty or underused nodes are consolidated away.
  • ✓Stateful workloads land on the tainted on-demand pool only.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Consolidation causes an outage

A single-replica service without a PodDisruptionBudget runs on a node that Karpenter consolidates during business hours.

terminal
$ kubectl -n catalog get events --sort-by=.lastTimestamp | tail -3
── what you'll see ──
Normal Evicted pod/catalog-6b9d... Evicted pod: node is being consolidated
Normal Scheduled pod/catalog-6b9d... Successfully assigned catalog/catalog-6b9d... to ip-10-20-11-5
Warning Unhealthy pod/catalog-6b9d... Readiness probe failed: connection refused

Break #2

Pods stay Pending forever

A team requests nvidia.com/gpu: 1 for an ML job.

terminal
$ kubectl get events --field-selector reason=FailedScheduling -A | tail -1; kubectl -n kube-system logs deploy/karpenter | grep -m1 incompatible
── what you'll see ──
Warning FailedScheduling 0/9 nodes are available: 9 Insufficient nvidia.com/gpu.
"could not schedule pod, incompatible with nodepool \"general\", no instance type satisfied resources {\"nvidia.com/gpu\":\"1\"} and requirements karpenter.k8s.aws/instance-category In [c m r]"

Part 5

Interview questions from this mission

01

How does Karpenter differ from Cluster Autoscaler?

02

How do you run workloads on Spot instances safely?

0/4 · 0%