skip to content

A CI job runs `helm template` and emits a different apiVersion than the cluster install does — why, and how do you fix the render?

level: seniorimportance: should knowfreq 44%

answer

  1. The render never asked a cluster
  2. Defaults compiled into the binary
  3. Describe the target on the command line
  4. One flag for version, one for the API list
  5. Pin the renderer or the default moves

basics

~20 s

With no cluster to query, helm template uses capabilities compiled into the Helm binary — a fixed Kubernetes version and a minimal API list — so capability branches take their fallback path. Pass --kube-version and --api-versions to describe the target cluster.

solid answer

~50 s

`helm template` makes no API call, so `.Capabilities` comes from a default baked into that Helm build: a Kubernetes version tracking the libraries it was compiled against, and an API list that does not enumerate what a real cluster serves. Most `.Capabilities.APIVersions.Has` tests therefore come back false and version comparisons are answered against the wrong number. The fix is to describe the target explicitly — `--kube-version` for the version, `--api-versions` for each group/version the cluster serves — and to keep those in the same per-environment configuration as the values files. Pin the Helm binary in CI as well, because the built-in default version moves between Helm releases and can change a render with no chart change. When the output depends on real discovery or on `lookup`, stop rendering offline and use a server-side dry run against the target cluster.

code

bash · 5 lines
bash
helm template gateway ingress-controller/ingress-controller \
  --values env/prod-gateway.yaml \
  --kube-version v1.31.4 \
  --api-versions policy/v1 \
  --api-versions autoscaling/v2

go deeper

for a junior

Remember that helm template renders on your machine with no cluster involved, and that flags exist to tell it which Kubernetes version and which APIs to assume.

for a middle

Explain the two flags precisely: --kube-version sets what .Capabilities.KubeVersion reports, --api-versions adds group/versions that Has will find, and neither implies the other.

for a senior

Show how you make CI renders trustworthy: per-environment version and API lists stored beside the values, a pinned Helm binary, and a server-side dry run when the answer must come from the cluster itself.

for a principal

Decide what an offline render may be used for across the organisation — a review artefact or a release gate — and make charts that cannot be rendered faithfully offline visible rather than quietly wrong.

This is the standard way an offline render lies to you, and it is worth walking through a concrete case. ### The incident shape A platform team installs a third-party ingress-controller chart from a public repository in front of a telemetry ingest gateway, driven by an 11-value override file per environment. CI renders the chart on every pull request and posts the diff for review. After a cluster upgrade the reviewed diff still shows the older beta apiVersion for one object, while the object that actually lands in the cluster carries the newer stable one. Nobody edited the chart; the two renders simply answered `.Capabilities` from different sources. ### Why the offline render differs `helm template` renders locally. It opens no connection to a cluster, which is the entire point: it works on a laptop with no kubeconfig and in a CI runner with no cluster credentials. But a chart branching on `.Capabilities` still has to be given answers, so Helm supplies a default capability set that is compiled into the binary. Its Kubernetes version tracks the client libraries that Helm build was compiled against — not your cluster — and its API list is a minimal set rather than the couple of hundred group/versions a real cluster advertises. The practical result: `Has` returns false for almost everything, and every version comparison is measured against a number nobody chose deliberately. ### Fixing the render Describe the target on the command line: ```bash helm template gateway ingress-controller/ingress-controller \ --values env/prod-gateway.yaml \ --kube-version v1.31.4 \ --api-versions policy/v1 \ --api-versions autoscaling/v2 ``` `--kube-version` sets what `.Capabilities.KubeVersion` reports. `--api-versions` adds entries that `.Capabilities.APIVersions.Has` will find, and it can be repeated. Note that neither flag implies the other: setting the version does not populate the API list, and listing APIs does not change the version. Charts that branch on both need both flags. Keep the pair beside the environment's values file, so the render is described in one reviewable place per environment rather than in ad-hoc pipeline arguments. ### Keeping the list honest A hand-written `--api-versions` list drifts the moment a cluster is upgraded or an operator is installed. Generate it instead: have a scheduled job read the served group/versions from each target cluster and commit the result next to that environment's values, so drift arrives as a reviewable diff rather than as a silently wrong render. Anything that must be exactly right at render time should not use the recorded list at all. ### When to stop rendering offline Two cases defeat the flags entirely. If the chart calls `lookup`, an offline render always sees an empty result, and no flag changes that. If the set of served APIs is large enough that maintaining a list is fiction, you are simulating a cluster badly. In both cases render through the API server instead: ```bash helm upgrade --install gateway ingress-controller/ingress-controller \ --namespace telemetry --values env/prod-gateway.yaml --dry-run=server ``` That needs cluster credentials in the pipeline, which is a real cost and a real decision — but it produces the manifest the cluster would actually get. ### Pin the renderer One more trap: the default capability set belongs to the binary. A CI image that silently picks up a newer Helm ships a newer default Kubernetes version, so a chart branching on `.Capabilities.KubeVersion` can render differently with no chart, values or flag change. Pin the Helm version in the image and treat upgrading it as a reviewed change, exactly as you would a dependency bump. If a render diff appears with no source change, check the renderer version before you check the chart. ### What the offline render is good for Even done well, an offline render is a review artefact, not a proof. It shows what the chart produces under stated assumptions. Say what those assumptions are — version, API list, Helm version — and reviewers can judge the diff; leave them implicit and everyone reads a manifest that no cluster will ever receive.

  • How do you keep the `--api-versions` list from drifting away from the real cluster?
    Generate it rather than hand-maintain it. A scheduled job reads the served group/versions from each target cluster and commits the list beside that environment's values file, so drift shows up as a reviewable diff. Anything that has to be exact at render time should use a server-side dry run instead of the recorded list.
  • The rendered diff still differs after passing both flags. What else could explain it?
    Anything else that is not a pure function of chart and values: a `lookup` call that returns empty offline, a different chart version resolved from the repository, values carried over from the previous release on an upgrade, or two different Helm binaries. Check the resolved chart version and the effective values before suspecting the flags.
  • Why pin the Helm version in the CI image if the chart has not changed?
    Because the default capabilities ship inside the binary. A newer Helm build carries a newer default Kubernetes version, so a chart branching on `.Capabilities.KubeVersion` can produce a different manifest with no chart, values or flag change. Pinning makes the render reproducible and turns a renderer upgrade into a reviewed change.

saying these in an interview costs you the question

  • Assumes `helm template` reads the current kubeconfig context
  • Thinks the built-in API list resembles a real cluster's
  • Believes `--kube-version` also populates the API list
  • Treats an offline render as proof of what will be applied
  • Ignores that the Helm binary version changes the default
  • Expects `--api-versions` to make `lookup` resolve

context