Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Kubernetes & Helm By James Joyner IV · · 10 min read

Helm Capabilities and kubeVersion Gating Across Clusters

One chart, many cluster versions. Helm .Capabilities and Chart.yaml kubeVersion let you render the right apiVersion everywhere — if you avoid the helm template trap.

  • #kubernetes-helm
  • #ai
  • #helm
  • #capabilities
  • #compatibility

I maintain a chart that installs onto clusters several Kubernetes minor versions apart, and the first version of it shipped a single hard-coded policy/v1beta1 PodDisruptionBudget. It worked beautifully in our newest cluster and failed at install time on an older one with a deprecation error, then later failed on a newer cluster when the beta API was finally removed. The chart was correct for exactly one cluster version and wrong for the rest. That’s the trap of writing manifests as if there’s only one Kubernetes.

Helm has the tools to fix this — .Capabilities checks and the kubeVersion constraint — but they interact in a way that lets bugs slip through CI and only appear on the real cluster. Knowing where that gap is matters as much as knowing the features.

Render based on what the cluster actually serves

The cleanest gate is .Capabilities.APIVersions.Has, which asks the target cluster which APIs it serves and lets you emit the matching apiVersion:

{{- if .Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" }}
apiVersion: policy/v1
{{- else }}
apiVersion: policy/v1beta1
{{- end }}
kind: PodDisruptionBudget
metadata:
  name: {{ include "app.fullname" . }}
spec:
  minAvailable: 1
  selector:
    matchLabels:
      {{- include "app.selectorLabels" . | nindent 6 }}

This is the right tool when a whole API group/version moved. The chart renders policy/v1 on clusters that serve it and falls back to the beta on older ones, with no hard-coded assumption about which cluster it’s running on.

Gate on version when a field, not an API, changed

Sometimes the apiVersion is stable but a field changed across versions — a feature that went GA and moved out from behind a flag. For that, compare against .Capabilities.KubeVersion:

{{- if semverCompare ">=1.29-0" .Capabilities.KubeVersion.Version }}
  unhealthyPodEvictionPolicy: AlwaysAllow
{{- end }}

The semverCompare has to be exactly right — an off-by-one in a >= either over-restricts (drops the field on a cluster that supports it) or under-restricts (emits a field the cluster rejects). This is precisely the kind of expression that needs a render test against each supported version, which we’ll get to.

Set a hard floor in Chart.yaml

The kubeVersion constraint is a blunt guardrail that makes helm install refuse to run on an out-of-range cluster up front, with a clear message instead of a server-side rejection:

# Chart.yaml
apiVersion: v2
name: app
version: 1.4.0
kubeVersion: ">=1.27.0-0 <1.32.0-0"

Use it to declare the range you actually test, not as your only safety mechanism. It blocks helm install, but — and this is the trap — it does not affect helm template.

The helm template trap

Here’s the part that costs people a production incident: helm template and helm install --dry-run do not talk to your cluster. They use Helm’s built-in capability defaults. So a chart can render perfectly in CI, pass review, and then fail on the real cluster because CI was rendering against Helm’s defaults, not your actual API surface.

The fix is to tell Helm what to pretend the cluster is, and to render every supported version in CI:

# Render as a 1.27 cluster
helm template app/ \
  --kube-version 1.27.0 \
  --api-versions policy/v1beta1/PodDisruptionBudget

# Render as a 1.31 cluster
helm template app/ \
  --kube-version 1.31.0 \
  --api-versions policy/v1/PodDisruptionBudget

Without --kube-version and --api-versions, CI is testing a fiction. A render matrix over your supported versions is the only way to know the gates actually do what you think.

The silently-dropped-resource bug

The nastiest failure mode of capability gating isn’t an error — it’s silence. A gate that evaluates false and emits nothing produces a chart that installs cleanly but is missing a resource. The HPA never gets created, autoscaling never happens, and there’s no error anywhere. Guard against it with an explicit failure branch, so an unsupported cluster gets a real message instead of a missing object:

{{- if .Capabilities.APIVersions.Has "autoscaling/v2/HorizontalPodAutoscaler" }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
# ...
{{- else }}
{{- fail "This chart requires autoscaling/v2 (Kubernetes >= 1.23). Upgrade the cluster or disable the HPA in values." }}
{{- end }}

Prompt: Here is a chart that ships an HPA, an Ingress, and a PDB, supporting Kubernetes 1.27 to 1.31. Add .Capabilities gates that pick the correct apiVersion per cluster, include a {{ fail }} branch where no supported API exists, set a kubeVersion constraint, and give me the helm template matrix commands to render each supported version. Render-and-review only — no install.

Output (excerpt): Added APIVersions.Has gates for networking.k8s.io/v1/Ingress, autoscaling/v2/HorizontalPodAutoscaler, and policy/v1/PodDisruptionBudget, each with a {{ fail }} fallback. Set kubeVersion: ">=1.27.0-0 <1.32.0-0". Matrix renders 1.27, 1.29, and 1.31 with matching --api-versions; each asserts the three resources are present so a false gate can’t silently drop one.

This is a strong fit for an AI assistant. It knows which APIs moved at which versions and can write both the conditionals and the render matrix, while I review the output for each version. I keep it render-and-review: the model produces templates and commands, and I run the matrix and diff the manifests before any install touches a cluster. The cluster-side counterpart — migrating off removed APIs — is covered in the Kubernetes & Helm guides.

Wrapping up

A chart that targets a fleet of cluster versions has to render the right APIs everywhere and fail loudly where it can’t. Use .Capabilities.APIVersions.Has to pick apiVersions, .Capabilities.KubeVersion with careful semver for field-level changes, and the Chart.yaml kubeVersion constraint as a floor — but never trust helm template without --kube-version and --api-versions, and always render every supported version in CI with assertions so a false gate can’t silently drop a resource. Let an AI assistant write the conditionals and the matrix; you review each rendered version. More upgrade-and-compatibility workflows are in the Kubernetes & Helm guides, with reusable starting points in the prompt library.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.