📐 VPA — Vertical Pod Autoscaler

The HPA scales horizontally (more pods). The VPA scales vertically — it watches a workload's actual resource usage and automatically adjusts the requests and limits on containers. This solves the most common cost problem: over-provisioned requests that waste money and block scheduling.

📊 Off mode

VPA only recommends new requests/limits — never applies them. Use this to understand what your pods actually need without disrupting production.

🔍 Initial mode

VPA applies recommendations only when pods are first created. No restarts of existing pods. Safe for stateful workloads.

⚡ Auto mode

VPA applies recommendations immediately — evicting pods to restart them with new resource values. Only use with multi-replica workloads where disruption is acceptable.

🎯 Recreate mode

Like Auto but only restarts pods when they are OOMKilled or exceed limits. Less disruptive than Auto.

Installing VPA

# Install VPA components (recommender, updater, admission controller)
git clone https://github.com/kubernetes/autoscaler.git
cd autoscaler/vertical-pod-autoscaler
./hack/vpa-install.sh

# Or via Helm
helm repo add fairwinds-stable https://charts.fairwinds.com/stable
helm install vpa fairwinds-stable/vpa --namespace vpa --create-namespace

VPA in Off mode — discover right-sizing

apiVersion: autoscaling.k8s.io/v1
kind: VerticalPodAutoscaler
metadata:
  name: checkout-api-vpa
  namespace: production
spec:
  targetRef:
    apiVersion: apps/v1
    kind:       Deployment
    name:       checkout-api
  updatePolicy:
    updateMode: "Off"   # recommend only — never evict
  resourcePolicy:
    containerPolicies:
    - containerName: app
      minAllowed:
        cpu:    50m
        memory: 64Mi
      maxAllowed:
        cpu:    4000m
        memory: 4Gi

# After running for 24h+, read the recommendations:
kubectl describe vpa checkout-api-vpa -n production
# VPA recommendation output:
# Recommendation:
#   Container Recommendations:
#     Container Name: app
#     Lower Bound:
#       cpu:    50m      ← minimum safe request
#       memory: 180Mi
#     Target:
#       cpu:    120m     ← recommended request (what VPA would set)
#       memory: 320Mi
#     Upper Bound:
#       cpu:    500m     ← maximum seen; set as limit
#       memory: 680Mi

# Your manifest was requesting: cpu: 1000m, memory: 1Gi
# VPA recommends:             cpu:  120m, memory: 320Mi
# Savings:                    88% CPU, 69% memory — typical for over-provisioned services
⚠️ VPA + HPA conflict Do not use VPA Auto mode and HPA on CPU/memory for the same workload simultaneously — they fight each other. Safe combinations: VPA Auto + HPA on custom metrics (KEDA), or VPA Off/Initial + HPA on CPU. The updateMode: "Off" pattern is safe with any HPA.

⚡ Karpenter — Next-Generation Node Provisioning

Karpenter (from AWS, now CNCF) takes a different approach to CA. Instead of managing pre-configured node groups, Karpenter provisions exactly the right instance type for each pending pod — directly calling the cloud API, no ASG required. Result: faster provisioning (~60s vs 3–5 min), better bin-packing, and dramatically simpler spot management.

Core concepts

NodePool

Defines constraints for nodes Karpenter can provision: instance families, zones, capacity type (spot/on-demand), OS, architecture. Karpenter picks the cheapest matching instance.

EC2NodeClass (AWS)

AWS-specific config: AMI family, subnet selectors, security groups, instance profile. Analogous to a launch template.

Just-in-time provisioning

When a pod is Pending, Karpenter evaluates its requirements (resources, affinity, topology) and launches the cheapest node that satisfies them — in ~60 seconds.

Consolidation

Karpenter continuously evaluates if nodes can be consolidated — evicting pods from underutilised nodes and replacing two small nodes with one larger cheaper one.

Karpenter NodePool — production configuration

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    metadata:
      labels:
        karpenter.sh/nodepool: default
    spec:
      nodeClassRef:
        apiVersion: karpenter.k8s.aws/v1beta1
        kind:       EC2NodeClass
        name:       default
      requirements:
      - key:      karpenter.sh/capacity-type
        operator: In
        values:   [spot, on-demand]   # prefer spot, fall back to OD
      - key:      kubernetes.io/arch
        operator: In
        values:   [amd64]
      - key:      karpenter.k8s.aws/instance-category
        operator: In
        values:   [c, m, r]          # compute, general, memory families
      - key:      karpenter.k8s.aws/instance-generation
        operator: Gt
        values:   ["2"]              # only recent generations
  limits:
    cpu:    "1000"    # max total CPU across all Karpenter nodes
    memory: 4000Gi
  disruption:
    consolidationPolicy: WhenUnderutilized
    consolidateAfter:    30s    # consolidate quickly in dev, longer in prod
    expireAfter:         720h   # rotate nodes every 30 days (security hygiene)
apiVersion: karpenter.k8s.aws/v1beta1
kind: EC2NodeClass
metadata:
  name: default
spec:
  amiFamily: AL2
  role:      KarpenterNodeRole-my-cluster
  subnetSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  securityGroupSelectorTerms:
  - tags:
      karpenter.sh/discovery: my-cluster
  blockDeviceMappings:
  - deviceName: /dev/xvda
    ebs:
      volumeSize: 100Gi
      volumeType: gp3
      encrypted:  true

⚖️ CA vs Karpenter — Decision Guide

DimensionCluster AutoscalerKarpenter
Provisioning speed3–5 minutes (ASG launch + bootstrap)~60 seconds (direct EC2 API)
Instance selectionPre-defined node groups onlyPicks cheapest from 300+ instance types
Spot managementNeeds separate node groups per typeAutomatic fallback across families/sizes
Bin-packingModerate — limited by node group sizesExcellent — right-sizes node to pod requests
ConsolidationLimited scale-down heuristicsActive consolidation (replaces 2 nodes with 1)
Cloud supportAll major clouds + on-premAWS (native), Azure/GCP (via community providers)
ComplexityModerate — manage node group configLower — declare constraints, Karpenter decides
MaturityVery mature, widely deployedProduction-ready since 0.30, rapidly adopted
🔵 The three-layer autoscaling stack Production clusters typically use all three layers together:
  • VPA (Off mode) — continuously recommends right-sized requests; apply manually or in Initial mode
  • HPA / KEDA — scales pod count based on load or event queues
  • Karpenter — provisions and removes nodes as pod count changes
Each layer handles a different dimension of the scaling problem.

🖥️ Cluster Autoscaler

The Cluster Autoscaler (CA) watches for unschedulable pods (Pending because no node has room) and adds nodes to the cluster. When nodes are underutilised and their pods can be moved, it removes them. CA works with cloud provider node groups (AWS ASGs, GCP MIGs, Azure VMSS).

Pod Pending ⚠️ Cluster Autoscaler watches unschedulable pods Cloud Provider ASG / MIG / VMSS New Node scale up ASG node joins

Installing CA on AWS EKS

# 1. Tag your Auto Scaling Groups (required for CA discovery)
# Tags: k8s.io/cluster-autoscaler/enabled = true
#       k8s.io/cluster-autoscaler/my-cluster = owned

# 2. Deploy CA (Helm)
helm repo add autoscaler https://kubernetes.github.io/autoscaler
helm install cluster-autoscaler autoscaler/cluster-autoscaler \
  --namespace kube-system \
  --set autoDiscovery.clusterName=my-cluster \
  --set awsRegion=us-east-1 \
  --set rbac.serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::123456789:role/cluster-autoscaler

# 3. Verify CA is running
kubectl logs -n kube-system -l app.kubernetes.io/name=aws-cluster-autoscaler --tail=20

Key CA configuration flags

# Critical flags to tune (set via helm --set or extraArgs)

--scale-down-delay-after-add=10m       # wait 10m after scale-up before evaluating scale-down
--scale-down-unneeded-time=10m          # node must be unneeded for 10m before removal
--scale-down-utilization-threshold=0.5 # scale down if node is <50% requested
--max-node-provision-time=15m          # give up waiting for node after 15m
--balance-similar-node-groups=true     # spread across AZs evenly
--skip-nodes-with-system-pods=true     # don't evict nodes with kube-system pods
--expander=least-waste                 # pick node group with smallest resource waste

Spot/preemptible nodes with CA

# Use multiple node groups: on-demand + spot, with priority expander
--expander=priority

# Priority ConfigMap: try spot first, fall back to on-demand
apiVersion: v1
kind: ConfigMap
metadata:
  name: cluster-autoscaler-priority-expander
  namespace: kube-system
data:
  priorities: |
    10:
      - .*spot.*           # lowest priority number = try first
    20:
      - .*on-demand.*      # fallback to on-demand
💡 CA scale-down is conservative by design CA waits 10 minutes before removing an underutilised node (configurable). This prevents thrashing. If you need faster scale-down in dev/staging, set --scale-down-unneeded-time=2m. In production, 10m is appropriate — the cost of keeping an extra node for a few minutes is negligible vs the disruption of premature scale-down.

🧠 Knowledge Check

Q1. You configure a VPA with updateMode: "Off" and run it for a week. The recommendation shows target CPU: 120m but your manifest has requests: 1000m. What is the correct next action?

A) Nothing — Off mode means VPA will apply the change at the next pod restart automatically
B> Enable Auto mode immediately to apply the 88% CPU reduction
C) Manually update your manifest to set cpu requests: 120m — VPA Off mode only recommends, never applies
D) Delete the VPA — it has already collected enough data

Q2. The Cluster Autoscaler added a new node 5 minutes ago and it's now at 30% utilization. Will CA remove it?

A) Yes — CA immediately reclaims underutilised nodes to minimise cost
B) No — scale-down-delay-after-add (default 10m) prevents scale-down evaluation for 10 minutes after scale-up
C> Only if a PodDisruptionBudget allows the eviction
D) Yes, if the node has no system pods

Q3. Why does Karpenter provision nodes ~60s faster than Cluster Autoscaler?

A) Karpenter uses pre-warmed node pools that keep spare capacity ready
B> Karpenter runs as a DaemonSet on every node, reducing coordinator latency
C) Karpenter calls EC2 RunInstances directly, bypassing the ASG pipeline that CA must wait for
D) Karpenter only provisions spot instances which have faster launch times

Q4. Why is using VPA Auto mode alongside HPA on CPU dangerous?

A> Both tools write to the same etcd key, causing API conflicts
B> VPA Auto requires a minimum of 3 replicas which conflicts with HPA minimum
C) VPA reducing CPU requests raises CPU utilisation %, triggering HPA scale-out — they fight in a feedback loop
D> Only dangerous for StatefulSets — safe for Deployments