Friday, 6 February 2026

Amazon EKS Autoscaling with Karpenter



Kubernetes autoscaling is a function that scales resources in and out depending on the current workload. AWS supports two autoscaling implementations:
  • Cluster Autoscaler
  • Karpenter 
    • Karpenter
    • flexible, high-performance Kubernetes cluster autoscaler
    • helps improve application availability and cluster efficiency
    • launches right-sized compute resources (for example, Amazon EC2 instances) in response to changing application load in under a minute
    • can provision just-in-time compute resources that precisely meet the requirements of your workload
    • automatically provisions new compute resources based on the specific requirements of cluster workloads. These include compute, storage, acceleration, and scheduling requirements. 
    • creates Kubernetes nodes directly from EC2 instances
    • improves the efficiency and cost of running workloads on the cluster
    • open-source


Pod Scheduler


  • Kubernetes cluster component responsible for determining which node Pods get assigned to
  • default Pod scheduler for Kubernetes is kube-scheduler
    • logs the reasons Pods can't be scheduled

Unschedulable Pods



A Pod is unschedulable when it's been put into Kubernetes' scheduling queue, but can't be deployed to a node. This can be for a number of reasons, including:
  • The cluster not having enough CPU or RAM available to meet the Pod's requirements.
  • Pod affinity or anti-affinity rules preventing it from being deployed to available nodes.
  • Nodes being cordoned due to updates or restarts.
  • The Pod requiring a persistent volume that's unavailable, or bound to an unavailable node.

How to detect unschedulable Pods?

Pods waiting to be scheduled are held in the "Pending" status, but if the Pod can't be scheduled, it will remain in this state. However, Pods that are being deployed normally are also marked as "Pending." The difference comes down to how long a Pod remains in "Pending." 

How to  fix unschedulable Pods? 
There is no single solution for unschedulable Pods as they have many different causes. However, there are a few things you can try depending on the cause. 
  • Enable cluster autoscaling
    • If you're using a managed Kubernetes service like Amazon EKS or Google Kubernetes Engine (GKE), you can very easily take advantage of autoscaling to increase and decrease cluster capacity on-demand. With autoscaling enabled, Kubernetes' Cluster Autoscaler will trigger your provider to add nodes when needed. As long as you've configured your cluster node pool and it hasn't reached its max node limit, your provider will automatically provision a new node and add it to the pool, making it available to the cluster and to your Pods.
  • Increase your node capacity
  • Check your Pod requests
  • Check your affinity and anti-affinity rules 

 

In this article we'll show how to enable cluster autoscaling with Karpenter.


How does the regular Kubernetes Autoscaler work in AWS?


When we create a regular Kubernetes cluster in AWS, each node group is managed by the AWS Auto-scaling group [Auto Scaling groups - Amazon EC2 Auto Scaling]. Cluster native autoscaler adjusts the desired size based on the load in the cluster to fit all unscheduled pods.

HorizontalPodAutoscaler (HPA) [Horizontal Pod Autoscaling | Kubernetes] is built into Kubernetes and it uses metrics like CPU usage, memory usage or custom metrics we can write to decide when to spin up or down additional pods in the node of the cluster. If our app is receiving more traffic, HPA will kick in and provision additional pods. 

VerticalPodAutoscaler (VPA) can also be installed in cluster where it manages the resource (like CPU and memory) allocation to pods that are already running.

What about when there's not enough capacity to schedule any more pods in the node? That's when we'll need an additional node. So we have a pod that needs to be scheduled but we don't know where to put it. We could call AWS API, spin up an additional EC2 node, get added it to our cluster or if we're using managed groups we can use Managed Node Group API, bump up the desired size but easier approach is to use cluster auto-scaler. There is a mature open-source solution called Cluster Auto-Scaler (CAS).

CAS was built to handle hundreds of different comninations of nodes types, zones, purchase options available in AWS. CAS works directly with managed node groups or self-managed managed nodes and auto-scaling groups which are AWS constructs to help us manage nodes. 


What are the issues with the regular Kubernetes Autoscaler?


Let's say CAS is installed on node, in cluster and manages one managed node group (MNG). It's filling up and we have an additional pod that needs to be provisioned so CAS tells MNG to bump up the number of nodes so it spins up another one so pod can now be scheduled. But this is not ideal. We have a single pod in a node, we don't need such a big node. 

This can be solved by creating a different MNG with a smaller instance type and now CAS recognizes that instance and provisions pod on a more appropriately-sized node.

Unfortunately, we might end up with many MNGs, based on requirements which might be a challenge to manage especially when looking best practices in terms of cost efficiency and high availability. 


How does Karpenter work?


Karpenter works differently, It doesn't use MNG or ASGs and manages each node directly. Let's say we have different pods, of different sizes. Let's say that HPA says that we need more of the smaller pods. Karpenter will intelligently pick the right instance type for that workload. If we need to spin up a larger pod it will again pick the right instance type. 

Karpenter picks exactly the right type of node for our workload. 

If we're using spot instances and spot capacity is not available, Karpenter does retries more quickly. Karpenter offers, faster, dynamic, more intelligent compute, using best practices without operational overhead of managing nodes ourselves. 

How to control how Karpenter operates?
There are many dimensions here. We can set constraints on Karpenter to limit the instances type, we can set up taints to isolate workloads to specific types of nodes. Different teams can have isolated access to different pods, one team can access billing pods, another GPU-based instances. 

Workload Consolidation feature: Pods are consolidated into fewer nodes.. let's say we have 3 nodes, two at 70% and one at 20% utilization. Karpenter detects this and will move pods from underutilized node to those two and shut down this now empty node (instances are terminated). This leads to lower costs.

Karpenter is making it easier to use spot and graviton instances which can also lead to lower costs. 

A feature to keep our nodes up to date. ttlSecondsUntilExpired parameter tells Karpenter to terminate nodes after a set amount of time. These nodes will automatically be replaced with new nodes, running the latest AMIs.

Karpenter:
1) lower costs
2) higher application availability 
3) lower operation overhead


Karpenter needs permissions to create EC2 instances in AWS. 

If we use a self-hosted (on bare metal boxes or EC2 instances), self-managed (you have full control over all aspects of Kubernetes) Kubernetes cluster, for example by using kOps (see also Is k8s Kops preferable than eks? : r/kubernetes), we can add additional IAM policies to the existing IAM role attached to Kubernetes nodes. 

If using EKS, the best way to grant access to internal service is with IAM roles for service accounts (IRSA).


How to configure Karpenter?


We can configure specific Karpenter NodePools or Provisioners.


How to know if node was provisioned by Karpenter?


Karpenter applies labels on nodes it provisions so let's check labels:

% kubectl get nodes --show-labels

If labels like karpenter.sh/nodepool or karpenter.sh/provisioner-name exist, Karpenter launched the node.

References:



Kubernetes Cluster Autoscaler


Kubernetes Cluster Autoscaler:
  • Designed to automatically adjust the number of nodes (EC2 instances) in our cluster based on the resource requests of the workloads running in the cluster
  • Kubernetes project, supported on EKS

Key Features:

  • Node Scaling: It adds or removes nodes based on the pending pods that cannot be scheduled due to insufficient resources.
  • Pod Scheduling: Ensures that all pending pods are scheduled by scaling the cluster up.

It works with EKS Managed Node Groups backed by AWS Auto Scaling Groups. In node group, if we provide specific settings (like custom block_device_mappings), EKS creates an EC2 Launch Template under the hood.



How to check if it's installed and enabled?


(1) Cluster Autoscaler usually runs as a Deployment in kube-system namespace so we can look for that deployment:

% kubectl get deployments -n kube-system | grep -i autoscaler   

cluster-autoscaler-aws-cluster-autoscaler   2/2     2            2           296d

We can also list pods directly:

% kubectl get pods -n kube-system | grep -i autoscaler

cluster-autoscaler-aws-cluster-autoscaler-7cbb844455-q2lxv 1/1 Running 0 206d
cluster-autoscaler-aws-cluster-autoscaler-7cbb844455-vhbsw 1/1 Running 0 206d

If we see a pod running, it’s installed.

Typical names:
  • cluster-autoscaler
  • cluster-autoscaler-aws-clustername
  • cluster-autoscaler-eks-...

(2) Inspect the Deployment (confirm it’s enabled & configured)

% kubectl describe deployment cluster-autoscaler -n kube-system

Name:                   cluster-autoscaler-aws-cluster-autoscaler
Namespace:              kube-system
CreationTimestamp:      Wed, 16 Apr 2025 12:25:38 +0100
Labels:                 app.kubernetes.io/instance=cluster-autoscaler
                        app.kubernetes.io/managed-by=Helm
                        app.kubernetes.io/name=aws-cluster-autoscaler
                        helm.sh/chart=cluster-autoscaler-9.46.6
Annotations:            deployment.kubernetes.io/revision: 1
                        meta.helm.sh/release-name: cluster-autoscaler
                        meta.helm.sh/release-namespace: kube-system
Selector:               app.kubernetes.io/instance=cluster-autoscaler,app.kubernetes.io/name=aws-cluster-autoscaler
Replicas:               2 desired | 2 updated | 2 total | 2 available | 0 unavailable
StrategyType:           RollingUpdate
MinReadySeconds:        0
RollingUpdateStrategy:  25% max unavailable, 25% max surge
Pod Template:
  Labels:           app.kubernetes.io/instance=cluster-autoscaler
                    app.kubernetes.io/name=aws-cluster-autoscaler
  Service Account:  cluster-autoscaler-aws-cluster-autoscaler
  Containers:
   aws-cluster-autoscaler:
    Image:      registry.k8s.io/autoscaling/cluster-autoscaler:v1.32.0
    Port:       8085/TCP
    Host Port:  0/TCP
    Command:
      ./cluster-autoscaler
      --cloud-provider=aws
      --namespace=kube-system
      --node-group-auto-discovery=asg:tag=k8s.io/cluster-autoscaler/enabled,k8s.io/cluster-autoscaler/mycorp-prod-mycluster
      --logtostderr=true
      --stderrthreshold=info
      --v=4
    Liveness:  http-get http://:8085/health-check delay=0s timeout=1s period=10s #success=1 #failure=3
    Environment:
      POD_NAMESPACE:     (v1:metadata.namespace)
      SERVICE_ACCOUNT:   (v1:spec.serviceAccountName)
      AWS_REGION:       us-east-1
    Mounts:             <none>
  Volumes:              <none>
  Priority Class Name:  system-cluster-critical
  Node-Selectors:       <none>
  Tolerations:          <none>
Conditions:
  Type           Status  Reason
  ----           ------  ------
  Progressing    True    NewReplicaSetAvailable
  Available      True    MinimumReplicasAvailable
OldReplicaSets:  <none>
NewReplicaSet:   cluster-autoscaler-aws-cluster-autoscaler-7cbb844455 (2/2 replicas created)
Events:          <none>


Key things to look for:
  • Replicas ≥ 1
  • No crash loops
  • Command args like:
    • --cloud-provider=aws
    • --nodes=1:10:nodegroup-name
    • --balance-similar-node-groups

If replicas are 0, it’s installed but effectively disabled.

(3) Check logs (is it actively scaling?)

This confirms it’s working, not just running.

% kubectl logs -n kube-system deployment/cluster-autoscaler


Healthy / active signs:
  • scale up
  • scale down
  • Unschedulable pods
  • Node group ... increase size

Red flags:
  • AccessDenied
  • no node groups found
  • failed to get ASG

(4) Check for unschedulable pods trigger

If CA is working, it reacts to pods stuck in Pending.

% kubectl get pods -A | grep Pending

If pods are pending and CA logs mention them → CA is enabled and reacting.

(5) AWS EKS-specific checks (very common)

a) Check IAM permissions (classic failure mode)

Cluster Autoscaler must run with an IAM role that can talk to ASGs.

% kubectl -n kube-system get sa | grep autoscaler

cluster-autoscaler-aws-cluster-autoscaler     0         296d
horizontal-pod-autoscaler                     0         296d

Let's inspect cluster-autoscaler-aws-cluster-autoscaler service accont:

% kubectl -n kube-system get sa cluster-autoscaler-aws-cluster-autoscaler  -o yaml

apiVersion: v1
automountServiceAccountToken: true
kind: ServiceAccount
metadata:
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::xxxxx:role/mycorp-prod-mycluster-cluster-autoscaler
    meta.helm.sh/release-name: cluster-autoscaler
    meta.helm.sh/release-namespace: kube-system
  creationTimestamp: "2026-04-16T11:25:37Z"
  labels:
    app.kubernetes.io/instance: cluster-autoscaler
    app.kubernetes.io/managed-by: Helm
    app.kubernetes.io/name: aws-cluster-autoscaler
    helm.sh/chart: cluster-autoscaler-9.46.6
  name: cluster-autoscaler-aws-cluster-autoscaler
  namespace: kube-system
  resourceVersion: "15768"
  uid: 0a7da521-1bf5-5a5f-a155-8801e876ea7b


Look for:

eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/ClusterAutoscalerRole

If missing → CA may exist but cannot scale.

b) Check Auto Scaling Group tags

Your node group ASGs must be tagged:

k8s.io/cluster-autoscaler/enabled = true
k8s.io/cluster-autoscaler/<cluster-name> = owned


Without these → CA runs but does nothing.

(6) Check Helm (if installed via Helm)

% helm list -A
NAME                  NAMESPACE      REVISION UPDATED      
cluster-autoscaler    kube-system    1           2025-04-16 12:25:30.389073326 +0100BST

STATUS   CHART                                APP VERSION
deployed cluster-autoscaler-9.46.6          1.32.0     


Then:

helm status cluster-autoscaler -n kube-system


The command helm list -A (or its alias helm ls -A) is used to list all Helm releases across every namespace in a Kubernetes cluster. Helm identifies your cluster and authenticates through the same mechanism as kubectl: the kubeconfig file. It uses the standard Kubernetes configuration file, typically located at ~/.kube/config, to determine which cluster to target.


(7) Double-check it’s not replaced by Karpenter

Many newer EKS clusters don’t use Cluster Autoscaler anymore.

% kubectl get pods -A | grep -i karpenter

kube-system karpenter-6f67b8c97b-lbq8p 1/1 Running     0       206d
kube-system karpenter-6f67b8c97b-wmprj 1/1 Running     0       206d


If Karpenter is installed, Cluster Autoscaler usually isn’t (or shouldn’t be).

Quick decision table

-----------------------------------------------------------------
Symptom                         Meaning
-----------------------------------------------------------------
No CA pod                         Not installed
Pod running, replicas=0         Installed but disabled
Logs show AccessDenied Broken IAM
Pods Pending, no scale-up ASG tags / config issue
Karpenter present                 CA likely not used
-----------------------------------------------------------------


Installation and Setup:


To use the Cluster Autoscaler in the EKS cluster we need to deploy it using a Helm chart or a pre-configured YAML manifest.

kubectl apply -f https://raw.githubusercontent.com/kubernetes/autoscaler/master/cluster-autoscaler/cloudprovider/aws/examples/cluster-autoscaler-autodiscover.yaml


In Terraform:

resource "helm_release" "cluster_autoscaler" {
  name = "cluster-autoscaler"

  repository = "https://kubernetes.github.io/autoscaler"
  chart      = "cluster-autoscaler"
  ...
}

Configuration:

  • Ensure the --nodes flag in the deployment specifies the min and max nodes for your node group.
  • Annotate your node groups with the k8s.io/cluster-autoscaler tags to enable autoscaler to manage them.

How to know if node was provisioned by Cluster Autoscaler?


Cluster Autoscaler applies labels on nodes it provisions so let's check labels:

% kubectl get nodes --show-labels

If label like eks.amazonaws.com/nodegroup exists, node was launched by and belongs to EKS Managed Node Group as Cluster Autoscaler launched the node.

Example:

% kubectl get nodes --show-labels

NAME                                     STATUS ROLES  AGE  VERSION            
ip-10-2-1-244.us-east-1.compute.internal Ready  <none> 206d v1.32.3-eks-473151a 

LABELS
Environment=prod,
beta.kubernetes.io/arch=amd64,
beta.kubernetes.io/instance-type=m5.xlarge,
beta.kubernetes.io/os=linux,
eks.amazonaws.com/capacityType=ON_DEMAND,
eks.amazonaws.com/nodegroup-image=ami-07fa6c030f5802c74,
eks.amazonaws.com/nodegroup=mycorp-prod-mycluster-20260714151819635800000002,
eks.amazonaws.com/sourceLaunchTemplateId=lt-0edc7a2b08ea82a28,
eks.amazonaws.com/sourceLaunchTemplateVersion=1,
failure-domain.beta.kubernetes.io/region=us-east-1,
failure-domain.beta.kubernetes.io/zone=us-east-1a,
mycorp;/node-type=default,
k8s.io/cloud-provider-aws=12b0e11196b7091c737cf66015f19720,
kubernetes.io/arch=amd64,
kubernetes.io/hostname=ip-10-2-1-244.us-east-1.compute.internal,
kubernetes.io/os=linux,
node.kubernetes.io/instance-type=m5.xlarge,
topology.ebs.csi.aws.com/zone=us-east-1a,
topology.k8s.aws/zone-id=use1-az1,
topology.kubernetes.io/region=us-east-1,
topology.kubernetes.io/zone=us-east-1a


If we list all nodegroups in the cluster, the one above is listed:

% aws eks list-nodegroups --cluster-name mycorp-prod-mycluster --profile my-profile
{
    "nodegroups": [
        "mycorp-prod-mycluster-20260714151819635800000002"
    ]
}



If cluster is overprovisioned, why Cluster Autoscaler doesn't scale nodes down automatically?


If Cluster Autoscaler is running but not shrinking the cluster, it's usually because:
  • System Pods: Pods like kube-dns or metrics-server don't have PDBs (Pod Disruption Budgets) and CA is afraid to move them.

  • Local Storage: A pod is using emptyDir or local storage.

  • Annotation: A pod has the "cluster-autoscaler.kubernetes.io/safe-to-evict": "false" annotation.

  • Manual Overrides: Check if someone manually updated the Auto Scaling Group (ASG) or the EKS Managed Node Group settings in the AWS Console. Terraform won't automatically "downgrade" those nodes until the next terraform apply or a node recycle.

  • If nodes are very old, they are "frozen" in time. Even if you changed your Terraform to smaller EC2 instances recently, EKS Managed Node Groups do not automatically replace existing nodes just because the configuration changed. They wait for a triggered update or a manual recycling of the nodes.


How to fix this overprovisioning?


Since your current Terraform state says you want e.g. 2 nodes of m5.large, but the reality is e.g. 4 nodes of m5.xlarge, you need to force a sync.

Step 1: Check for Drift

Run a terraform plan. It will likely show that it wants to update the Launch Template or the Node Group version to switch from xlarge back to large.

Step 2: Trigger a Rolling Update

If you apply the Terraform and nothing happens to the existing nodes, you need to tell EKS to recycle them. You can do this via the AWS CLI:

aws eks update-nodegroup-version \
    --cluster-name <your-cluster-name> \
    --nodegroup-name <your-nodegroup-name> \
    --force

Note: This will gracefully terminate nodes one by one and replace them with the new m5.large type defined in your TF.



Cluster Autoscaler VS Karpenter


While both tools scale Kubernetes nodes to meet pod demand, they use fundamentally different approaches. Cluster Autoscaler (CA) is the traditional, "group-based" tool that adds nodes to existing pools, whereas Karpenter is a "provisioning" tool that directly creates the specific instances your applications need. 


Quick Feature Comparison Table


Scaling Logic
  • Cluster Autoscaler (CA): Scales pre-defined node groups (ASGs)
  • Karpenter: Directly provisions individual EC2 instances.

Speed
  • Cluster Autoscaler (CA): Slower; waits for cloud provider group updates
  • Karpenter: Faster; provisions nodes in seconds via direct APIs.

Cost Control
  • Cluster Autoscaler (CA): Limited; uses fixed node sizes in groups.
  • Karpenter: High; picks the cheapest/optimal instance for the pod.

Complexity
  • Cluster Autoscaler (CA): Higher; must manage multiple node groups.
  • Karpenter: Lower; one provisioner can handle many pod types.

Key Differences


Infrastructure Model:
  • CA asks, "How many more of these pre-configured nodes do I need?". 
  • Karpenter asks, "What specific resources (CPU, RAM, GPU) does this pending pod need right now?" and builds a node to match.

Node Groups: 
  • CA requires you to manually define and maintain Auto Scaling Groups (ASGs) for different instance types or zones. 
  • Karpenter bypasses ASGs entirely, allowing it to "mix and match" instance types dynamically in a single cluster.

Consolidation: 
  • Karpenter actively monitors the cluster to see if it can move pods to fewer or cheaper nodes to save money (bin-packing). 
  • While CA has a "scale-down" feature, it is less aggressive at optimizing for cost.

Spot Instance Management: 
  • Karpenter handles Spot interruptions and price changes more natively, selecting the most stable and cost-efficient Spot instances in real-time.

Which should you choose?


Use Cluster Autoscaler if you need a stable, battle-tested solution that works across multiple cloud providers (GCP, Azure) or if your workloads are very predictable and don't require rapid scaling.

Use Karpenter if you are on AWS EKS, need to scale up hundreds of nodes quickly, want to heavily use Spot instances, or want to reduce the operational burden of managing dozens of node groups.

Disable Cluster Autoscaler if you plan to use Karpenter. Having both leads to race conditions and wasted cost.

When to Run Both Together

It's generally not recommended to run Cluster Autoscaler and Karpenter together in the same cluster. However, there are specific scenarios where it might be acceptable:

Valid use cases for running both:

  • Migration period: Transitioning from Cluster Autoscaler to Karpenter, where you temporarily run both while gradually moving workloads
  • Hybrid node management: Managing distinct, non-overlapping node groups where Cluster Autoscaler handles some node groups and Karpenter handles others (though this adds complexity)

When It's Not Recommended (and Why)

Primary reasons to avoid running both:

Conflicting decisions: Both tools make independent scaling decisions, which can lead to:

  • Race conditions where both try to provision nodes simultaneously
  • Inefficient resource allocation
  • Unpredictable scaling behavior
  • One tool removing nodes the other just provisioned

Increased operational complexity:

  • Two systems to monitor, troubleshoot, and maintain
  • Doubled configuration overhead
  • More difficult to understand which tool made which scaling decision

Resource contention: Both tools consume cluster resources and API server capacity, adding unnecessary load.

No significant benefits: Karpenter can handle everything Cluster Autoscaler does, often more efficiently, so there's rarely a technical need for both.

EKS-Specific Considerations

Tthe same principles apply to AWS EKS clusters, with some additional context:

EKS particularities:

  • Karpenter was designed specifically for AWS/EKS and integrates deeply with EC2 APIs
  • Karpenter typically provides better performance on EKS (faster provisioning, better bin-packing)
  • If you're on EKS, the general recommendation is to choose Karpenter over Cluster Autoscaler for new deployments

Migration best practice for EKS: If migrating from Cluster Autoscaler to Karpenter on EKS, ensure they manage completely separate node groups, and complete the migration as quickly as feasible to minimize the period of running both.


How to migrate pods from nodes deployed by Cluster Autoscaler to those deployed by Karpenter?


If you'd rather use Karpenter for everything, you should eventually set your min_size, max_size, and desired_size to 0 in this node group and let Karpenter handle the provisioning instead.



Kubernetes Metrics Server

 


Kubernetes Metrics Server is a foundational component required by several other critical cluster modules and tools: 

1. Horizontal Pod Autoscaler (HPA)

2. Vertical Pod Autoscaler (VPA) 
  • Purpose: While HPA adds more pods, the Vertical Pod Autoscaler (VPA) adjusts the CPU and memory requests/limits of existing pods.
  • Dependency: VPA relies on Metrics Server for the real-time resource data it uses to recommend or apply these resource changes. 

2. Native CLI Observability (kubectl top) 
  • Purpose: Commands used for ad-hoc debugging and performance monitoring.
  • Dependency: Both kubectl top pods and kubectl top nodes query the Metrics API directly. Without the server, these commands will return an error. 

3. Kubernetes Dashboard 
  • Purpose: A web-based UI for managing and troubleshooting clusters.
  • Dependency: The Kubernetes Dashboard uses Metrics Server to display resource usage graphs and live statistics for nodes and pods. 

4. Third-Party Monitoring Tools & Adapters
  • Custom Metrics Adapters: Some adapters that bridge external sources (like CloudWatch or Datadog) to Kubernetes may use the standard Metrics API for fallback or basic resource data.
  • Resource Management Tools: Operational tools such as Goldilocks, which suggests "just right" resource requests, often depend on the baseline metrics provided by this server. 

Key Distinction


While the Metrics Server is essential for these control loops (HPA, VPA), it is not a replacement for a full observability stack like Prometheus. It only stores a short-term, in-memory snapshot and does not provide historical data

How to to install the Metrics Server as an EKS Community Add-on to enable these features?


In March 2025, AWS introduced a new catalog of community add-ons that includes the Metrics Server. This allows you to manage it directly through EKS-native tools like any other AWS-managed add-on (e.g., VPC CNI or CoreDNS). 

Method 1: Using the AWS Management Console


The easiest way to install it is through the EKS console: 
  • Navigate to your EKS cluster in the AWS Console.
  • Select the Add-ons tab and click Get more add-ons.
  • Scroll down to the Community add-ons section.
  • Find Metrics Server, select it, and click Next.
  • Choose the desired version (usually the latest recommended) and click Create. 

Method 2: Using the AWS CLI


You can also install the community add-on via the command line:

aws eks create-addon \
  --cluster-name <YOUR_CLUSTER_NAME> \
  --addon-name metrics-server

Verification


Once the installation status moves to Active, verify that the pods are running in the kube-system namespace: 

kubectl get deployment metrics-server -n kube-system

Finally, test that the Metrics API is responding:

kubectl top nodes

Note: If you are using AWS Fargate, you may need to update the containerPort from 10250 to 10251 in the deployment configuration to ensure compatibility with Fargate's networking constraints. 


Metrics Server Configuration



To configure custom resource limits for the Metrics Server EKS community add-on, you can use Configuration Values during installation or update. This is essential for high-pod-count clusters where the default allocation may lead to OOMKilled errors. 

1. Scaling Recommendations


The Metrics Server's resource consumption scales linearly with your cluster's size. Baseline recommendations include: 
  • CPU: Approximately 1 millicore per node in the cluster.
  • Memory: Approximately 2 MB of memory per node.
  • Large Clusters: If your cluster exceeds 100 nodes, it is recommended to double these defaults and monitor performance. 

2. How to Apply Custom Limits


You can provide a JSON or YAML configuration block via the AWS EKS Add-ons API. 

Via AWS CLI


Use the configuration-values flag to pass your resource overrides:

aws eks create-addon \
  --cluster-name <YOUR_CLUSTER_NAME> \
  --addon-name metrics-server \
  --configuration-values '{
    "resources": {
      "requests": { "cpu": "100m", "memory": "200Mi" },
      "limits": { "cpu": "200m", "memory": "500Mi" }
    }
  }'


Via AWS Console

  • Go to the Add-ons tab in your EKS cluster.
  • Click Edit on the metrics-server add-on.
  • Expand the Optional configuration settings.
  • Paste the JSON configuration into the Configuration values text box. 

3. Critical Configuration for High Traffic


In addition to resource limits, you may want to adjust the scraping frequency to make HPA more responsive.

  • Metric Resolution: The default is 60s. For faster scaling, add --metric-resolution=15s to the container arguments via the same configuration block.
  • High Availability: The community add-on defaults to 2 replicas to prevent downtime during scaling events. 



Thursday, 5 February 2026

Horizontal Pod Autoscaler (HPA)

 


The Horizontal Pod Autoscaler (HPA) serves to automatically align your application's capacity with its real-time demand by adjusting the number of pod replicas. Its operation depends on several critical components and configurations within an EKS or Kubernetes cluster. 

Kubernetes Horizontal Pod Autoscaler (HPA) is a built-in Kubernetes controller
  • standard Kubernetes autoscaling mechanism
  • Available Out of the box (i.e., without installing third-party controllers)

It's "standard" in the sense that the feature is built into the Kubernetes control plane, but it isn't "automatic" in the sense that it guesses which of your apps need scaling.

Think of it like a Thermostat: The thermostat (HPA Controller) is already installed on the wall (EKS Control Plane), but it won't turn on the AC until you tell it what the Target Temperature (CPU/Memory threshold) is and which room (Deployment) to monitor.

Here is why a manifest is required for every app:

1. The Controller vs. The Resource

The Controller (The "How"): This is a loop running inside the EKS Control Plane. It is always active, waiting for instructions. Kubernetes HPA Documentation explains this loop.
The Resource (The "What"): The HPA Manifest is that instruction. It tells the controller: "Watch Deployment X, keep CPU at 50%, and don't go above 10 pods."

2. Manual Intent

Kubernetes follows a Declarative Model. It never assumes you want to scale. If it scaled every pod automatically, a single bug in your code (like an infinite loop) could scale your cluster to 1,000 nodes and drain your AWS budget instantly. You must explicitly opt-in by creating the HPA resource.

3. Unique Criteria for Every App

Not all apps scale the same way:
  • Web API: Might scale when CPU hits 70%.
  • Background Worker: Might scale based on Memory usage.
  • Data Processor: Might scale based on a Custom Metric like SQS queue depth.

Summary: What is "Standard"?

What is standard is the API definition and the Controller. What is not standard is your specific application's scaling logic.

To see what the HPA Controller is looking for, you can check your Deployment's resource requests via kubectl:

kubectl get deployment <name> -o yaml | grep resources -A 5


Key Features:

  • Pod Scaling: Adjusts the number of pod replicas to match the demand.
  • Automatically scales up/down the number of pods in a deployment, replication controller, or replica set based on observed CPU utilization, memory or other selected custom/external metrics.

Purpose

  • Dynamic Scalability: Automatically adds pods during traffic surges to maintain performance and removes them during low-traffic periods to reduce waste.
  • Cost Optimisation: Ensures you only pay for the compute resources currently needed rather than over-provisioning for peak loads.
  • Resilience & Availability: Prevents application crashes and outages by proactively scaling out before resources are fully exhausted.
  • Operational Efficiency: Replaces manual intervention with "architectural definition," allowing infrastructure to manage itself based on predefined performance rules. 

Dependencies


HPA cannot function on its own; it requires the following "links" and infrastructure: 

  • Metrics Server (The Aggregator): This is the most critical infrastructure dependency. The HPA controller queries the Metrics API (typically provided by the Metrics Server) to get real-time CPU and memory usage data.
  • Resource Requests (The Baseline): For the HPA to calculate percentage-based utilization (e.g., "scale at 50% CPU"), the target Deployment must have resources.requests defined. Without these, the HPA has no 100% baseline to measure against and will show an unknown status.
  • Controller Manager: The HPA logic runs as a control loop within the Kubernetes kube-controller-manager, which periodically (every 15 seconds by default) evaluates the metrics and updates the desired replica count.
  • Scalable Target: The HPA must be linked to a resource that supports scaling, such as a Deployment, ReplicaSet, or StatefulSet.
  • Cluster Capacity (Node Scaling): While HPA scales pods, it depends on an underlying node scaler (like Karpenter or Cluster Autoscaler) to provide new EC2 instances if the cluster runs out of physical space for the additional pods. 

Dependencies

  • It requires Kubernetes Metrics Server 

Installation and Setup:


To use HPA ensure the Metrics Server is installed in your cluster to provide resource metrics.

kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml

Configuration:


Create an HPA resource for your deployment.

kubectl autoscale deployment your-deployment --cpu-percent=50 --min=1 --max=10

How to check if it's enabled?


% kubectl api-resources -o wide | grep autoscaling

NAME SHORTNAMES APIVERSION NAMESPACED KIND
VERBS CATEGORIES
...
horizontalpodautoscalers  hpa  autoscaling/v2 true HorizontalPodAutoscaler  create,delete,deletecollection,get,list,patch,update,watch all


In which namespace do HorizontalPodAutoscalers reside in?


In AWS EKS, HorizontalPodAutoscalers (HPA) are namespaced resources, meaning they belong in the same namespace as the workload (e.g., Deployment or StatefulSet) they are intended to scale. 

While there is no single "HPA namespace," here is how they are distributed and where related components live:

1. The HPA Resource Namespace 

  • Application Namespace: When you create an HPA, you define it within the specific namespace where your application is running (e.g., default, production, or demo).
  • Constraint: An HPA can only scale a target resource (like a Deployment) that exists in that same namespace. 

2. Infrastructure & Metrics Namespaces 

While the HPA resource lives with your app, the supporting infrastructure often resides in system namespaces: 

  • Metrics Server: This is a mandatory prerequisite for HPA on EKS. It is typically deployed in the kube-system namespace.
  • Custom Metrics Adapters: If you are scaling based on custom metrics (like Prometheus or CloudWatch), components like the prometheus-adapter or k8s-cloudwatch-adapter may be installed in kube-system or a dedicated namespace like custom-metrics.
  • Cluster Autoscaler: Often confused with HPA, the Cluster Autoscaler (which scales EC2 nodes rather than pods) also typically resides in the kube-system namespace. 

To find all HPAs across your entire EKS cluster, you can run:

kubectl get hpa -A

We might have an output like this:

% kubectl get horizontalpodautoscalers -A    
No resources found

It is possible to get "No resources found" for several reasons, despite the resource being namespaced. This usually means that while the API type exists, no actual instances of that resource have been created in your EKS cluster yet.

Why you see "No resources found":
  • HPA is not yet createdBy default, EKS clusters do not come with any HorizontalPodAutoscalers pre-configured. You must explicitly create one for your application.
  • Metrics Server Missing: HPAs rely on the Kubernetes Metrics Server to function. While the HPA object can be created without it, it will show a status of <unknown> and may not appear if you are looking for active scaling.
  • Namespace Context: Even with -A (all namespaces), if no user or system service has defined an HPA resource, the list will be empty. 

How to Verify and Fix:
  • Check if Metrics Server is running:
    • Run kubectl get deployment metrics-server -n kube-system. If it’s missing, you can install it via the AWS EKS Add-ons in the console or via kubectl apply.
        kubectl get all -A | grep metrics-server 
  • Check API availability:
    • Run kubectl api-resources | grep hpa to confirm the cluster recognizes the resource type.
  • Create a test HPA:
    • If you have a deployment named my-app, try creating one: 
        kubectl autoscale deployment my-app \
            --cpu-percent=50 --min=1 --max=10


Note: If you are using a newer version of EKS (like 1.31) with Auto Mode, some autoscaling is handled automatically by the control plane, but standard HPAs still need to be manually defined if you want pod-level scaling based on custom metrics. 

% kubectl get all -A | grep metrics-server 

default pod/metrics-server-5db5f64c66-sjd2p        1/1     Running     0          205d
default service/metrics-server  ClusterIP       172.21.76.224    <none>  443/TCP  95d
default deployment.apps/metrics-server             1/1     1            1         295d
default replicaset.apps/metrics-server-5db5f64c66   1         1         1         295d

This behavior occurs because no instances of HorizontalPodAutoscaler (HPA) have been created yet, even though the supporting infrastructure (Metrics Server) and API are active. 

In Kubernetes, the presence of the metrics-server and the autoscaling/v2 API resource does not mean an HPA is automatically running for your appsYou must manually define an HPA for each deployment you want to scale. 

Why kubectl get hpa -A is empty
  • Workloads are not yet auto-scaled: By default, EKS (and Kubernetes) does not apply HPAs to your deployments. You must explicitly create an HPA object that references your target Deployment or StatefulSet.
  • kubectl get all exclusion: Standard kubectl get all does not include HPAs in its output, which is why your previous command didn't show them even if they existed.
  • Namespace Location: While your metrics-server is in the default namespace (though typically it's in kube-system), HPAs must be created in the same namespace as the app they are scaling. 

How to create your first HPA

If you have a deployment (e.g., named my-deployment) in the default namespace, you can create an HPA for it using this command:

kubectl autoscale deployment my-deployment --cpu-percent=50 --min=1 --max=10


Verification Steps

Once created, verify it with the following:
  • List all HPAs: kubectl get hpa -A.
  • Check Metrics Flow: Since your metrics-server is running, ensure it is actually collecting data by running kubectl top pods -A. If this returns usage data, your HPA will be able to scale correctly. 

Note: For HPA to function, your Deployment must have resource requests (specifically cpu) defined in its container spec, or the HPA will show <unknown> targets. 



How to configure metrics it needs to observe?


In AWS EKS, you set criteria for scaling in the spec section of a HorizontalPodAutoscaler (HPA) resource. You define thresholds through two primary blocks: metrics (to trigger scaling) and behavior (to control the rate and stability of scaling). 

1. Setting Thresholds (metrics)

The HPA calculates the required number of replicas based on the gap between current usage and your target. 
  • Target Utilization: Typically set as a percentage of a pod's requested CPU or memory.
  • Where to define: Inside the metrics list in your HPA manifest. 

metrics:
- type: Resource
  resource:
    name: cpu
    target:
      type: Utilization
      averageUtilization: 60 # Scale when average CPU exceeds 60% of requests



2. Setting Scaling Speed (behavior)

Advanced scaling logic is set in the behavior block, allowing you to fine-tune how fast the cluster grows or shrinks. 
  • Stabilization Window: Prevents "flapping" by making the HPA wait and look at past recommendations before acting.
    • Scale-Up: Default is 0 seconds (instant growth).
    • Scale-Down: Default is 300 seconds (5 minutes) to ensure a spike is truly over before killing pods.
  • Policies: Restrict the absolute number or percentage of pods changed within a specific timeframe. 

behavior:
  scaleUp:
    stabilizationWindowSeconds: 0
    policies:
    - type: Percent
      value: 100
      periodSeconds: 15 # Double replicas every 15 seconds if needed
  scaleDown:
    stabilizationWindowSeconds: 300
    policies:
    - type: Pods
      value: 1
      periodSeconds: 60 # Remove only 1 pod per minute for stability


3. Critical Prerequisites
  • Resource Requests: You must define resources.requests in your Deployment manifest. HPA cannot calculate utilization percentages without this baseline.
  • Metrics Server: Must be running in your cluster (usually in kube-system or default) to provide the data HPA needs.


To link an HPA to your Deployment, the HPA uses a scaleTargetRef. This acts like a pointer, telling the HPA controller exactly which resource to watch and resize.

1. Ensure your Deployment has "Requests"

The HPA cannot calculate percentages (like "50% CPU") unless the Deployment defines what 100% looks like. Check your Deployment for a resources.requests block:

# Inside your Deployment manifest
spec:
  containers:
  - name: my-app
    image: my-image
    resources:
      requests:
        cpu: "250m"    # HPA uses this as the 100% baseline
        memory: "512Mi"

Use code with caution.

2. The HPA Manifest

Create a file named hpa.yaml. The scaleTargetRef is the "link" that connects it to your app.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
  namespace: default # MUST be the same as your deployment
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-deployment-name # <--- This is the "Link"
  minReplicas: 2
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60 # Target 60% of requested CPU


3. Apply and Verify

Run the following commands to put the link into action:

Apply the manifest:

kubectl apply -f hpa.yaml

Check the link:

kubectl get hpa

Wait about 30-60 seconds. If you see TARGETS: <unknown>/60%, the Metrics Server is still calculating.
If you see TARGETS: 0%/60% (or a specific number), the link is successful!

Pro Tip - The Quick Link: If you don't want to write YAML, you can create this link instantly via the CLI:

kubectl autoscale deployment my-deployment-name --cpu-percent=60 --min=2 --max=10


HPA vs Karpenter


In AWS EKS, it is perfectly normal to have Karpenter running without any HorizontalPodAutoscaler (HPA) manifests. This happens because they solve two entirely different scaling problems: 

1. Karpenter vs. HPA: The "Supply" vs. "Demand" Gap

  • HPA manages Pods (The Demand): It decides how many pods you need (e.g., "CPU is high, let's go from 2 pods to 5 pods").
  • Karpenter manages Nodes (The Supply): It provides the underlying infrastructure for those pods. It watches for pods that are "Pending" because there is no room for them, then quickly spins up a new EC2 instance that fits them perfectly. 

If you have no HPAs, it means your application replica counts are currently static (e.g., always 3 pods). Karpenter is only "scaling" when you manually change that number or when you deploy a new app that needs more room. 

2. Can pods be adjusted automatically without HPA? 

Yes, there are a few other ways pod counts or resources can be adjusted:
  • Vertical Pod Autoscaler (VPA): Instead of adding more pods, VPA adjusts the CPU and Memory limits of your existing pods based on actual usage.
  • KEDA (Kubernetes Event-driven Autoscaling): Often used instead of standard HPA for complex triggers. It can scale pods to zero and back up based on external events like AWS SQS queue depth, Kafka lag, or Cron schedules.
  • GitOps/CD Pipelines: Sometimes scaling is "automated" via external CI/CD tools (like ArgoCD) that update the replica count in your git repo based on specific triggers or schedules rather than in-cluster metrics. 

3. Why you might want to add HPA

Without HPA, Karpenter is essentially a "just-in-time" provisioning tool for a static workload. If your traffic spikes, your pods might crash from resource exhaustion before Karpenter has a reason to act. Adding HPA allows your app to "request" more pods, which then triggers Karpenter to "supply" more nodes. 

To handle traffic spikes, HPA and Karpenter work as a two-stage relay: 
  • HPA (The Demand): Triggers when CPU/Memory usage spikes, creating "Pending" pods that cannot fit on current nodes.
  • Karpenter (The Supply): Sees those "Pending" pods and immediately provisions new EC2 instances to accommodate them. 

The Combined YAML Example

This configuration sets up an application to scale up during spikes and ensures Karpenter has the right "instructions" to provide nodes. 

Part A: The Workload (Deployment) 

You must define resource requests so HPA has a baseline and Karpenter knows what size node to buy. 

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spike-app
spec:
  replicas: 2
  template:
    spec:
      containers:
      - name: web-server
        image: nginx
        resources:
          requests:
            cpu: "500m"    # Crucial: HPA uses this for % calculation
            memory: "512Mi" # Crucial: Karpenter uses this to select EC2 size

Part B: The Scaling Rule (HPA)

This tells Kubernetes to add pods when the existing ones are busy. 

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: spike-app-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: spike-app
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 60  # Scale up at 60% to give Karpenter time to boot nodes

Part C: The Node Provisioner (Karpenter NodePool)

This tells Karpenter which AWS instances are "allowed" for your scaling pods. 

apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: default
spec:
  template:
    spec:
      requirements:
        - key: "karpenter.sh/capacity-type"
          operator: In
          values: ["spot", "on-demand"] # Use Spot to save money during spikes
        - key: "karpenter.k8s.aws/instance-category"
          operator: In
          values: ["c", "m", "r"]
      nodeClassRef:
        name: default
  disruption:
    consolidationPolicy: WhenUnderutilized # Automatically kill nodes when pods scale down

Why this works for spikes

  • Buffer Time: Setting the HPA to 60% (instead of 90%) ensures you start scaling before pods are overwhelmed, giving Karpenter ~60 seconds to join new nodes to the cluster.
  • Just-in-Time Nodes: Unlike the old Cluster Autoscaler, Karpenter doesn't wait for "Node Groups" to update; it calls the EC2 Fleet API directly to get exactly what your pending pods need.
  • Automatic Cleanup: When the spike ends, HPA reduces pod counts. Karpenter's consolidationPolicy then notices the nodes are empty and terminates them to stop AWS billing. 

In Kubernetes, "700m" stands for 700 millicores.

It is a unit of measurement for CPU processing power, where 1000m is equal to 1 vCPU (or 1 Core). Therefore, 700m is 0.7 of a vCPU.

How it works in your HPA:

Since your HPA is using type: AverageValue (instead of Utilization), it is looking at the raw CPU usage rather than a percentage:
  1. The Trigger: The HPA controller calculates the average CPU usage across all currently running pods in that deployment.
  2. The Action: If the average usage exceeds 700m, the HPA will add more pods to spread the load and bring that average back down to 700m.

Why use AverageValue instead of AverageUtilization?

  • AverageUtilization (Percentage): Requires you to have resources.requests defined. It scales based on "percentage of what I asked for."
  • AverageValue (Raw Number): Does not technically require a request baseline to function. It scales based on "absolute CPU consumed." This is useful if your app has a hard performance limit (e.g., "This app starts lagging if it hits 0.7 cores") regardless of what the pod's requested limit is.

Pro-Tip for Karpenter users:

When using AverageValue, ensure your Deployment's CPU request is set to something sensible (like 800m or 1000m). If your request is lower than your HPA target (e.g., request is 500m but target is 700m), your pods will constantly throttle before they ever trigger a scale-up! Kubernetes Resource Management provides more details on these units.

Horizontal Pod Autoscaler and Upgrading Kubernetes version of the cluster


When upgrading your Kubernetes cluster version, the most critical Horizontal Pod Autoscaler (HPA) considerations involve API version deprecations, metrics server compatibility, and newly introduced scaling configuration fields.

1. API Version Deprecations & Removals

Kubernetes frequently matures its APIs, meaning older HPA versions are deprecated and eventually removed. 
  • autoscaling/v2 is now GA (General Availability): As of Kubernetes v1.23, the autoscaling/v2 API version is stable and generally available.
  • Removal of v2beta2: The autoscaling/v2beta2 version was removed in v1.26. If your manifests still use this version, they will fail to apply or update in clusters v1.26 and newer.
  • Manifest Updates: You must update the apiVersion in your YAML files. Note that fields like targetAverageUtilization in beta versions were replaced by a more structured target block in the stable v2 API. 

2. Metrics Server & Infrastructure

The HPA depends on external components that may also require updates during a cluster upgrade. 
  • Metrics Server Compatibility: Ensure your Metrics Server version is compatible with your new Kubernetes version. Without it, HPA cannot fetch CPU or memory data, and scaling will fail.
  • Custom Metrics Adapters: if you use custom metrics (e.g., via Prometheus), ensure your Prometheus Adapter supports the new Kubernetes API version, as some older adapters may still attempt to call removed API endpoints. 

3. New Features and Behaviors

Upgrading allows you to leverage newer scaling controls that improve stability: 
  • Configurable Scaling Behavior: Introduced in v1.18 and matured in later versions, the behavior field allows you to set a stabilizationWindowSeconds for scale-up and scale-down independently. This is essential for preventing "flapping" (rapidly scaling up and then down).
  • Configurable Tolerance: In very recent versions (e.g., v1.33), you can now fine-tune the 10% default tolerance. Previously, HPA would only act if the metric differed by more than 10%; you can now adjust this for more sensitive or coarser scaling needs. 

4. Best Practices for the Upgrade Process

  • Audit Before Upgrading: Use tools like Kube-no-trouble (kubent) or Pluto to find resources using deprecated HPA APIs.
  • Dry Runs: Run kubectl apply --dry-run=client on your HPA manifests against the target cluster version to catch schema errors before they impact production.
  • Monitor Events: After upgrading, watch HPA events using kubectl get events --field-selector involvedObject.kind=HorizontalPodAutoscaler to ensure it is still successfully fetching metrics and making decisions. 

When moving from the deprecated autoscaling/v2beta2 (removed in v1.26) to the stable autoscaling/v2 (available since v1.23), the primary change is the unification of target fields. 

YAML Comparison

The stable v2 API replaces direct target fields (like averageUtilization) with a nested target block. 

Feature Deprecated autoscaling/v2beta2 Stable autoscaling/v2
-------     -------------------------------  ---------------------- 
API Version apiVersion: autoscaling/v2beta2 apiVersion: autoscaling/v2

CPU Target   averageUtilization: 50 target:type:Utilization averageUtilization: 50
Custom Target averageValue: 100 target:type:AverageValue averageValue: 100


Comparison Example:

A comparison of the YAML structure shows how the apiVersion changes and the resource target is nested within a target block in the v2 version. You can see the full YAML example in the referenced documents. 

Key Migration Notes
  • Seamless Conversion: The Kubernetes API server can convert between these versions, allowing you to use kubectl get hpa <name> -o yaml --output-version=autoscaling/v2 to view HPAs in the new format.
  • Manifest Updates: While conversion is possible, you must update your CI/CD pipelines and YAML manifests to use autoscaling/v2 before upgrading to v1.26 to prevent errors.
  • Behavior Block: The behavior block remains the same structurally, but using the v2 API is required for long-term support.


To identify which HPAs in your cluster are using deprecated API versions, you can use built-in kubectl commands or specialized open-source tools.
 

1. Using Built-in kubectl Commands


While kubectl doesn't have a single "find-deprecated" flag, you can use these methods to audit your resources:

Audit via API Server Warnings (v1.19+):

The API server automatically sends a warning header when you access a deprecated endpoint. Simply listing them often triggers a warning in the console if they use deprecated APIs:

kubectl get hpa -A


Dry-Run Manifest Validation:

Before applying an update, use a client-side dry-run to see if the manifest will be accepted by the new cluster version's schema.

kubectl apply -f your-hpa.yaml --dry-run=client



Check Metrics for Requested Deprecated APIs:

You can query the API server's raw metrics to see if any client (like an old CI/CD script) is still requesting deprecated HPA versions.

kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

 
2. Using Specialized Audit Tools (Recommended)

Specialized tools are the most reliable way to find exactly which resources are affected before an upgrade. 

Kube-no-trouble (kubent):

This tool scans your live cluster and lists exactly which resources are using APIs scheduled for removal.

# Install and run (requires no cluster installation)
sh -c "$(curl -sSL https://git.io/install-kubent)"
kubent


Pluto:

While kubent scans the live cluster, Pluto is best for scanning your Helm charts and static YAML files in your git repository to catch issues before they are deployed.

# Scan local directory
pluto detect-files -d .

 
3. Quick Check of Supported Versions 

To see which API versions your cluster currently supports for horizontal scaling, use the following command: 

kubectl api-versions | grep autoscaling


Note: If you are upgrading to v1.26 or newer, any HPA using autoscaling/v2beta2 must be updated to autoscaling/v2, as the older version will no longer be served


How to use kubectl convert to automatically upgrade your existing YAML manifests to the latest API version?


The kubectl convert command is no longer part of the standard kubectl binary; it is now a standalone plugin. You must install it to automatically upgrade your HPA manifests from v2beta2 to v2. 

1. Install the kubectl-convert Plugin 

Choose the method that matches your operating system:
macOS (via Homebrew):

brew install kubectl-convert


Manual Download (Linux/macOS/Windows):

Download the binary for your architecture from the official Kubernetes release page and move it to your system path (e.g., /usr/local/bin/kubectl-convert).

Verification:

Run kubectl convert --help to confirm the plugin is active. 

2. Convert Your HPA Manifests

Once installed, you can use the command to rewrite your old YAML files to the stable autoscaling/v2 version. 

Convert a Specific File:

This command reads your v2beta2 file and outputs a clean v2 version to your terminal.

kubectl convert -f old-hpa.yaml --output-version autoscaling/v2

Save the Converted File:

kubectl convert -f old-hpa.yaml --output-version autoscaling/v2 > new-hpa.yaml

Bulk Conversion (Directory):

You can point it to a directory containing multiple manifests to update them all at once.

kubectl convert -f ./my-hpa-folder/ --output-version autoscaling/v2

 
3. Alternative: Direct Export from the Cluster

Because the Kubernetes API server internally handles conversion between versions, you can "live-convert" an existing HPA by explicitly requesting the target version during a get command: 

kubectl get hpa.v2.autoscaling <hpa-name> -o yaml > upgraded-hpa.yaml

This method is often faster if the HPA is already running in your cluster, as it bypasses the need for the convert plugin. 


How to use Kustomize to handle these API version changes across multiple environments?



Kustomize allows you to manage the transition from v2beta2 to v2 across multiple environments (e.g., Dev, Staging, Prod) by layering environment-specific changes over a common base configuration. 

1. Structure Your Directory

Keep your primary HPA manifest in a base folder and create overlays for each environment. 


├── base
│   ├── hpa.yaml
│   └── kustomization.yaml
└── overlays
    ├── dev
    │   └── kustomization.yaml
    └── prod
        ├── hpa-v2-patch.yaml
        └── kustomization.yaml

2. Strategy for Phased Upgrades

If you are upgrading clusters one by one, you can use Kustomize patches to change the apiVersion only for specific environments while keeping others on the older version. 

Example: Upgrading 'Prod' to v2

If your base/hpa.yaml still uses v2beta2, you can create a patch in your Prod overlay to upgrade it to v2 without touching the base file used by other environments. 

overlays/prod/hpa-v2-patch.yaml:

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 60

overlays/prod/kustomization.yaml:

resources:
  - ../../base
patches:
  - path: hpa-v2-patch.yaml
    target:
      kind: HorizontalPodAutoscaler
      name: my-app-hpa

3. Validating the Conversion

Before applying changes to a live cluster, use the following commands to ensure Kustomize has correctly merged the new apiVersion and schema: 

View Rendered YAML:

kubectl kustomize overlays/prod

Diff Against Live Cluster:

Use kubectl diff to see exactly what will change in the API server.

kubectl diff -k overlays/prod

 
4. Best Practices

Keep Base "Newest": Once all clusters are upgraded, move the v2 configuration into the base and remove the patches from your overlays to keep your code DRY.
CI/CD Integration: Use Pluto in your CI pipeline to scan the output of kustomize build for any remaining deprecated API versions.