Helm 3 arrived in 2019 and became the default way to package Kubernetes applications. Helm 4, released
at the end of 2025, is the first major version since. Major versions make people nervous, but the Helm
maintainers kept chart compatibility as a priority: existing apiVersion: v2 charts install with Helm 4.
What changes is mostly how Helm talks to the cluster and how it is extended.
We migrated forty internal charts and three deployment pipelines over a month. This post is the set of notes we wish we had at the start. Treat specific flags and defaults as things to check against the release notes of the exact Helm 4 version you install; minor releases have refined several of them.
What did not change
Start with the reassuring part:
- Charts with
apiVersion: v2work without modification. A new chart API version is being developed separately and is not required. - Releases installed with Helm 3 are visible to and upgradable by Helm 4; release storage in Secrets is compatible.
- Templating functions, values files and
helm templateoutput for our charts were byte-for-byte identical in our tests, except for a handful of ordering differences in debug output.
That meant the migration risk was concentrated in the apply step and in tooling around Helm.
Server-side apply
Helm 3 applied resources with a client-side three-way merge: it compared the previous release manifest, the new manifest and the live object, and sent patches. Helm 4 supports Kubernetes server-side apply (SSA), where the API server performs the merge and tracks which manager owns each field.
What this changes in practice:
- Field ownership is explicit. Fields set by Helm are owned by Helm's field manager. If another
actor (an operator,
kubectl edit, a HorizontalPodAutoscaler setting replicas) owns a field, conflicts become visible instead of being silently overwritten or silently left alone. - Drift handling is more predictable. Manual edits to Helm-owned fields are corrected on the next upgrade, and fields Helm does not set are left to their owners.
- Conflicts can fail an upgrade. When Helm tries to set a field another manager owns, the API server reports a conflict. Helm can force ownership, which is usually what you want for fields declared in the chart.
Check how your Helm 4 version chooses between server-side and client-side apply for new installs and for
releases created by Helm 3, and whether you want to set it explicitly with --server-side in pipelines.
We set it explicitly so behaviour did not depend on defaults.
The conflicts we hit
Two kinds of conflicts showed up in staging:
spec.replicason Deployments with an HPA. Several charts setreplicaseven when autoscaling was enabled. With SSA, Helm and the HPA both claimed the field. Fix: renderreplicasonly when autoscaling is disabled.
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
- Annotations added by other controllers on Services and Ingresses. Charts that templated an annotations map as a whole were fine; the conflict appeared only where a chart and a controller set the same annotation key. We removed those keys from the charts.
Both were latent bugs in our charts that Helm 3 had papered over.
Waiting for resources
helm install --wait and helm upgrade --wait in Helm 3 had their own readiness logic for a fixed set
of resource kinds. Helm 4 moved to status checking based on kstatus, the same library used by other
Kubernetes tools, which understands standard conditions and works for custom resources that follow the
conventions.
Effects we saw:
- Waiting for Deployments, StatefulSets and Jobs behaved the same or better.
- Waiting now worked for some custom resources, such as cert-manager
Certificateobjects, which Helm 3 ignored. One chart started waiting much longer than before, because aCertificatetook minutes to be issued in staging. That was correct behaviour, but it exceeded our pipeline's--timeout. - The wait strategy is selectable in Helm 4; check
helm upgrade --helpfor the available values in your version if you need the old behaviour temporarily.
Plugins and post-renderers
Helm 4 introduced a reworked plugin system, including support for plugins built as WebAssembly modules, and post-renderers are now delivered as plugins rather than arbitrary executables passed with a flag.
We used two Helm 3 plugins (helm-diff and helm-secrets) and one post-renderer running Kustomize.
Before migrating CI images, we checked each plugin's compatibility with Helm 4, upgraded to versions that
supported it, and converted the post-renderer to the plugin form. This was the single biggest piece of
work in the migration, and it is worth doing first, because CI fails loudly when a plugin does not load.
CLI and output changes
A few smaller changes affected scripts:
- Some deprecated flags from Helm 3 were removed. A search of our pipeline code for every
helminvocation found two. - Log output moved to structured logging, which changed the format of some messages our scripts parsed.
Scripts that grepped Helm's human-readable output were changed to use
--output jsonwhere available. helm versionoutput format differs, which broke a version check in one pipeline.
Our test plan
We ran every chart through this sequence in a staging cluster before switching production pipelines:
- Render diff.
helm templatewith Helm 3 and Helm 4 for every environment's values, diffed withdyff. Expected: no differences. - Upgrade an existing Helm 3 release with Helm 4. Using the same chart version, so any change comes from Helm, not the chart. Expected: no resource changes, release history continues.
- Upgrade with a real chart change. Expected: only the intended changes, no SSA conflicts.
- Rollback.
helm rollbackto the previous revision. Expected: success, workloads healthy. - Install from scratch in an empty namespace with
--wait. Expected: completes within the pipeline timeout. - Uninstall. Expected: all resources removed, including those with hooks.
Steps 2 and 3 found the HPA and annotation conflicts; step 5 found the certificate wait.
Rollout order
- Developers' machines and local scripts (Helm 4 CLI installed side by side as
helm4). - CI images for non-production pipelines.
- Argo CD: check which Helm version your Argo CD release uses for rendering; Argo CD renders templates and applies them itself, so SSA and wait changes in the Helm CLI do not apply there in the same way.
- Production pipelines, one at a time.
We kept Helm 3 available in CI images for a month as a fallback and removed it afterwards.
Common mistakes
| Mistake | Effect | Do this instead |
|---|---|---|
| Upgrading CI before checking plugins | Every pipeline fails on plugin load | Inventory and upgrade plugins first |
Ignoring SSA conflicts with --force everywhere |
Controllers' fields overwritten | Fix chart templates that set fields others own |
| Keeping old pipeline timeouts | Waits on new resource kinds time out | Review --timeout per chart |
| Parsing human-readable Helm output | Scripts break on format changes | Use JSON output |
| Assuming Argo CD changes with the CLI | Different behaviour in GitOps | Check Argo CD's bundled Helm and apply mode |
Key takeaways
- Helm 4 keeps
apiVersion: v2charts and Helm 3 releases compatible; risk is in apply, wait and tooling. - Server-side apply exposes field ownership conflicts that Helm 3 hid, often around
replicaswith HPAs. - kstatus-based waiting covers more resource kinds, which can lengthen waits.
- Plugins and post-renderers need the most preparation.
- A render diff plus upgrade, rollback and install tests on staging catches almost everything.
References
- Helm documentation: https://helm.sh/docs/
- Helm releases and changelogs: https://github.com/helm/helm/releases
- Kubernetes, Server-Side Apply: https://kubernetes.io/docs/reference/using-api/server-side-apply/
- kstatus: https://github.com/kubernetes-sigs/cli-utils/blob/master/pkg/kstatus/README.md
Comments
0No comments yet. Be the first to comment!
Sign In to post a comment.