Kubernetes API Deprecation Guide#
Kubernetes deprecates and removes API versions on a predictable schedule. When an API version is removed, any manifests or Helm charts using the old version will fail to apply on the upgraded cluster. Workloads already running are not affected – they continue to run – but you cannot create, update, or redeploy them until the manifests are updated. This guide walks through the complete workflow for detecting and fixing deprecated APIs before an upgrade.
How Kubernetes API Deprecation Works#
Kubernetes follows a strict deprecation policy:
- GA APIs (v1) are never removed within a major version.
- Beta APIs (v1beta1, v2beta1) are deprecated when a stable version is introduced and removed after a minimum of 3 minor releases or 9 months, whichever is longer.
- Alpha APIs (v1alpha1) can be removed in any release without notice.
The practical consequence is that every Kubernetes minor version upgrade potentially removes API versions that were deprecated 2-3 versions earlier. If you skip versions, the risk multiplies.
Notable API Removals by Version#
| Removed In | Old API | Replacement |
|---|---|---|
| 1.22 | networking.k8s.io/v1beta1 Ingress | networking.k8s.io/v1 |
| 1.22 | rbac.authorization.k8s.io/v1beta1 | rbac.authorization.k8s.io/v1 |
| 1.25 | policy/v1beta1 PodDisruptionBudget | policy/v1 |
| 1.25 | batch/v1beta1 CronJob | batch/v1 |
| 1.26 | autoscaling/v2beta2 HorizontalPodAutoscaler | autoscaling/v2 |
| 1.27 | storage.k8s.io/v1beta1 CSIStorageCapacity | storage.k8s.io/v1 |
| 1.29 | flowcontrol.apiserver.k8s.io/v1beta2 | flowcontrol.apiserver.k8s.io/v1 |
| 1.32 | flowcontrol.apiserver.k8s.io/v1beta3 | flowcontrol.apiserver.k8s.io/v1 |
This table is not exhaustive. Always check the release notes for your target version.
Step 1: Identify Your Target Version#
Before scanning for deprecated APIs, know exactly where you are and where you are going:
# Current cluster version
kubectl version --short 2>/dev/null || kubectl version
# Check available upgrade versions (managed services)
# EKS
aws eks describe-cluster --name my-cluster --query 'cluster.version'
# GKE
gcloud container get-server-config --zone us-central1-a --format="yaml(validMasterVersions)"
# AKS
az aks get-upgrades --resource-group myRG --name myCluster -o tableRecord your current version and target version. You will need both when running deprecation scanners.
Step 2: Scan In-Cluster Resources with Pluto#
Pluto scans live cluster resources and Helm releases for deprecated API versions. It checks what is actually deployed, not just what is in your Git repo.
Install Pluto#
# macOS
brew install FairwindsOps/tap/pluto
# Linux
curl -L -o pluto.tar.gz \
"https://github.com/FairwindsOps/pluto/releases/latest/download/pluto_linux_amd64.tar.gz"
tar -xzf pluto.tar.gz && sudo mv pluto /usr/local/bin/Scan All In-Cluster Resources#
# Detect deprecated APIs targeting a specific Kubernetes version
pluto detect-all-in-cluster --target-versions k8s=v1.31
# Example output:
# NAME KIND VERSION REPLACEMENT REMOVED DEPRECATED REPL AVAIL
# my-ingress Ingress networking.k8s.io/v1beta1 networking.k8s.io/v1 true true true
# my-hpa HorizontalPodAutoscaler autoscaling/v2beta2 autoscaling/v2 true true trueThe columns that matter are REMOVED (will break on the target version) and REPL AVAIL (whether the replacement API exists in your current version so you can migrate now).
Scan Helm Releases#
Helm stores the last applied manifest in its release secrets. Pluto can scan these directly:
pluto detect-helm --target-versions k8s=v1.31
# This catches cases where the live resource was converted by the API server
# but the Helm release still references the old API version.
# On next helm upgrade, it will try to apply the old version and fail.This is a critical distinction. The API server automatically converts stored objects to the latest version, so kubectl get may show networking.k8s.io/v1 even though the Helm release template still uses v1beta1. The next helm upgrade will break.
Step 3: Scan with Kubent for a Second Opinion#
Kubent (kube-no-trouble) is an alternative scanner. Running both tools catches edge cases that either tool might miss.
Install Kubent#
sh -c "$(curl -sSL https://git.io/install-kubent)"Run Kubent#
kubent --target-version 1.31
# Output:
# 4:17PM INF >>> Deprecated APIs <<<
# KIND VERSION NAMESPACE NAME REPLACEMENT SCOPE
# Ingress networking/v1beta1 production my-ingress networking.k8s.io/v1 CLUSTERKubent also scans Helm releases and can scan local manifest files:
# Scan local files
kubent -f ./manifests/ --target-version 1.31
# Scan a Helm chart's rendered output
helm template my-release ./my-chart | kubent --target-version 1.31Step 4: Check API Server Metrics for Runtime Usage#
Even after scanning manifests, clients might be calling deprecated APIs at runtime:
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apisThis catches controllers, operators, and CI/CD pipelines that use deprecated APIs programmatically.
Step 5: Update Manifests#
Once you have a list of deprecated APIs, update each manifest. The changes are usually straightforward – replace the apiVersion field and adjust any fields that changed between versions.
Ingress: v1beta1 to v1#
The Ingress API had significant structural changes between beta and stable:
# OLD: networking.k8s.io/v1beta1
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
name: my-ingress
annotations:
kubernetes.io/ingress.class: nginx
spec:
rules:
- host: app.example.com
http:
paths:
- path: /
backend:
serviceName: my-service
servicePort: 80
# NEW: networking.k8s.io/v1
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
ingressClassName: nginx # annotation replaced by field
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix # required in v1, did not exist in v1beta1
backend:
service:
name: my-service # restructured from flat fields
port:
number: 80Key differences: ingressClassName replaces the annotation, pathType is now required (use Prefix, Exact, or ImplementationSpecific), and the backend structure is nested differently.
Pure apiVersion Changes (No Structural Differences)#
These resources only require changing the apiVersion field. The spec is identical:
| Resource | Old | New |
|---|---|---|
| HorizontalPodAutoscaler | autoscaling/v2beta2 | autoscaling/v2 |
| CronJob | batch/v1beta1 | batch/v1 |
| PodDisruptionBudget | policy/v1beta1 | policy/v1 |
Using kubectl-convert for Automated Conversion#
kubectl krew install convert
kubectl convert -f old-ingress.yaml --output-version networking.k8s.io/v1 > new-ingress.yamlAlways review the converted output. The tool handles structural changes but may not preserve comments or formatting.
Step 6: Update Helm Charts#
For Helm-managed resources, update the templates in your chart source and bump the chart version.
Fix Helm Release Metadata#
If a Helm release was deployed with old API versions, helm upgrade will fail even if the chart templates are updated. The release metadata stored in the cluster still references the old API version. Use the mapkubeapis Helm plugin to fix this:
# Install the plugin
helm plugin install https://github.com/helm/helm-mapkubeapis
# Update release metadata to use current API versions
helm mapkubeapis my-release -n my-namespace
# Now helm upgrade will work
helm upgrade my-release ./my-chart -n my-namespaceThis is the single most common source of Helm upgrade failures after a Kubernetes version bump.
Step 7: Test on a Non-Production Cluster#
Never upgrade production first. Validate on a test cluster at the target version:
# Server-side dry-run validates API versions against the actual API server
kubectl apply --dry-run=server -f manifests/
# Confirm zero deprecated APIs remain
pluto detect-all-in-cluster --target-versions k8s=v1.31Use --dry-run=server, not --dry-run=client. Client-side dry-run does not validate API versions against the server.
Step 8: Build a Pre-Upgrade Checklist#
Combine all the above into a repeatable checklist for every upgrade:
#!/bin/bash
TARGET_VERSION="v1.31"
echo "=== Pre-Upgrade API Deprecation Check ==="
echo "Target: $TARGET_VERSION"
echo ""
echo "--- Pluto: In-Cluster Scan ---"
pluto detect-all-in-cluster --target-versions "k8s=$TARGET_VERSION"
echo ""
echo "--- Pluto: Helm Releases ---"
pluto detect-helm --target-versions "k8s=$TARGET_VERSION"
echo ""
echo "--- Kubent ---"
kubent --target-version "${TARGET_VERSION#v}"
echo ""
echo "--- API Server Deprecated API Usage ---"
kubectl get --raw /metrics 2>/dev/null | grep -c apiserver_requested_deprecated_apis || echo "No deprecated API calls detected"
echo ""
echo "--- Helm Releases Status ---"
helm list -A --output json | jq -r '.[] | "\(.namespace)/\(.name) chart=\(.chart) status=\(.status)"'Run this script before every upgrade. If it reports zero findings, proceed with confidence. If it reports findings, fix them before touching the cluster version.
Common Pitfalls#
Third-party Helm charts using old APIs. Update the chart version before upgrading the cluster. If the chart maintainer has not updated, fork the chart and fix the templates yourself.
CRDs with deprecated API versions. Operator-managed CRDs sometimes reference deprecated APIs in their conversion webhooks or stored versions. Check the operator’s compatibility matrix for your target Kubernetes version.
CI/CD pipelines hardcoding API versions. Search pipeline definitions for hardcoded apiVersion strings. These break silently when the cluster is upgraded.
GitOps repositories with stale manifests. If you use ArgoCD or Flux, scan the Git repository, not just the cluster. The next sync from Git will try to apply old versions even if the cluster auto-converted stored resources.