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.
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/
- addons/
- karpenter/
- karpenter-app.yamlnew
- nodepools.yamlnew
Part 3
Build it, step by step
- 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.karpenteraws 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
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
Define the node pools
generalallows Spot and on-demand across many instance families (flexibility is what makes Spot reliable) with a CPU limit as a cost guardrail.consolidationPolicy: WhenEmptyOrUnderutilizedlets Karpenter repack pods.budgetslimit how many nodes can be disrupted at once. A secondon-demandpool 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
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=0kubectl set resources deploy inflate --requests=cpu=1kubectl scale deploy inflate --replicas=20kubectl get nodeclaims -w── expected output ──NAME TYPE CAPACITY ZONE NODE READYgeneral-8x2kq c7g.4xlarge spot ap-south-1b Unknowngeneral-8x2kq c7g.4xlarge spot ap-south-1b ip-10-20-12-87.ap-south-1.compute.internal True - 5
Scale down and see consolidation
After
consolidateAfter, Karpenter terminates the empty node.kubectl get eventsshows its reasoning.terminal$ kubectl scale deploy inflate --replicas=0kubectl get events -A --field-selector source=karpenter --sort-by=.lastTimestamp | tail -2── expected output ──Normal DisruptionTerminating nodeclaim/general-8x2kq Disrupting NodeClaim: Empty/DeleteNormal 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.
Break #2
Pods stay Pending forever
A team requests nvidia.com/gpu: 1 for an ML job.
Part 5
Interview questions from this mission
How does Karpenter differ from Cluster Autoscaler?
How do you run workloads on Spot instances safely?