Three gaps that appear when moving from Helm to Kustomize — and how to fill them again: template functions, hooks, and tests
When I first heard talk of replacing Helm with Kustomize, I was honestly relieved. If you've ever pulled an all-nighter debugging syntax like {{ toYaml .Values.something | indent 8 }}, or felt the frustration of having to run helm template just to see what YAML actually ends up in your cluster, you'll understand that feeling.
But once you actually start the migration, you hit a wall quickly. Some things Helm provided have no equivalent concept in Kustomize at all. Three gaps hurt the most: dynamic template functions, release lifecycle hooks, and helm test-based validation. How you fill these three gaps determines whether the migration succeeds or fails.
This post explores how to fill each gap — what tools and patterns to combine — grounded in real use cases. Not "Kustomize does everything" and not "Helm is better after all," but the realistic options that work in practice as of 2026.
Why Is This Migration Conversation Happening Now
Kustomize's philosophy is clear: template-free. Base YAML is just plain Kubernetes manifests, and you build the final state by layering environment-specific overlays on top. kubectl explain works fully, IDE autocompletion stays alive, and running kubectl kustomize overlays/prod immediately shows you the final YAML that will enter the cluster.
Helm, on the other hand, is a full templating system — Go's text/template with the Sprig function library on top. Hundreds of functions like if/else, range, trim, toYaml, and randAlphaNum run inside YAML pipelines. Powerful, but complexity comes with it.
The dominant pattern among teams running large-scale clusters in 2026 is this: Helm for external package boundaries, Kustomize for internal application configuration. A hybrid strategy where third-party charts like Prometheus or Cert-Manager stay in Helm, while deployment manifests the team owns directly use Kustomize.
But when you move team-owned deployment code to Kustomize, gaps appear.
Gap 1 — Template Functions
What Actually Disappears
Template logic commonly used in Helm charts falls into roughly four categories:
| Type | Helm Example | Kustomize Support |
|---|---|---|
| Simple value substitution | {{ .Values.image.tag }} |
Partially possible (replacements) |
| Conditional resource inclusion | {{- if .Values.ingress.enabled }} |
Components as a substitute |
| Dynamic value generation | {{ randAlphaNum 32 }} |
Completely impossible, external tools required |
| YAML block insertion | {{ toYaml .Values.resources | indent 10 }} |
Can be replaced with patches |
Pattern A — Pre-rendering with the helmCharts Field
Since Kustomize 4.1, declaring a helmCharts field in kustomization.yaml lets you insert Helm rendering as the first step in the Kustomize pipeline. All templates including Sprig functions are processed first, and then Kustomize patches are applied on top of that pure YAML output.
# kustomization.yaml
helmCharts:
- name: my-app
releaseName: my-app-prod
repo: https://charts.example.com
version: 1.2.3
valuesFile: values.yaml
patches:
- path: patch-resources.yamlOne important caveat: helmCharts is a disabled feature by default and won't be processed at all without the --enable-helm flag. It's the first thing people get stuck on in real migrations, so keep it in mind.
kustomize build --enable-helm overlays/prod
# or
kubectl kustomize --enable-helm overlays/prodAlso, if you omit releaseName, the chart name is used as the release name for rendering — so if it differs from your actual release name, labels and selectors may not match expectations.
To achieve a similar effect on the command line:
helm template my-app-prod ./chart --values values.yaml | kubectl apply -f -At first I thought this approach was "Kustomize admitting defeat," but thinking about it more, using Helm's packaging capability and Kustomize's patching capability each for what they do best is actually quite sensible. Committing rendered output to Git also makes drift detection much clearer.
Pattern B — Simple Value Substitution with replacements
Simple cases like injecting an image tag or ConfigMap value into another resource can be handled natively with Kustomize's replacements.
# kustomization.yaml
replacements:
- source:
kind: ConfigMap
name: app-config
fieldPath: data.IMAGE_TAG
targets:
- select:
kind: Deployment
name: my-app
fieldPaths:
- spec.template.spec.containers.[name=app].imageSpecifying containers by numeric index (containers.0.image) will parse, but if container order changes, it can silently inject the value into the wrong target — dangerous. The official docs recommend name-based selectors like [name=app].
That said, replacements is a pattern for "referencing a value from another resource," not executing arbitrary expressions like Go templates. Dynamic generation like randAlphaNum 32 is impossible here too.
Pattern C — Conditional Resources with Components
Conditional inclusion patterns like {{- if .Values.ingress.enabled }} map naturally to Kustomize Components (4.1+). Cut each feature unit into an independent kind: Component and reference it only from the overlays that need it.
# components/ingress/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- ingress.yaml
patches:
- path: enable-tls.yaml# overlays/prod/kustomization.yaml
components:
- ../../components/ingress
resources:
- ../../baseYou can express the intent "this environment has ingress enabled" directly in code without straining the overlay hierarchy. The more conditional resource inclusions a chart has, the greater the benefit of adopting Components.
Dynamic Secret Generation — A Problem Predating Kustomize
Secret auto-generation using randAlphaNum is actually an anti-pattern even in Helm, not just a Kustomize limitation. It's well known that running helm upgrade regenerates values each time, replacing secrets and restarting referencing workloads along with them. There's a workaround using the lookup function, but it doesn't fit well with GitOps flows.
In other words, adopting an external secret management tool isn't "the price you pay to migrate to Kustomize" — it's an improvement you'd need to make eventually regardless of your tool choice. Bundling it with your migration plan actually gives you a stronger justification.
| Tool | Characteristics |
|---|---|
| External Secrets Operator | Syncs from Vault, AWS Secrets Manager, etc. to Kubernetes Secrets |
| Sealed Secrets (Bitnami) | Encrypted secrets committed to Git, GitOps-friendly |
| Vault Agent Injector | Sidecar-based injection directly into Pods |
Gap 2 — Release Lifecycle Hooks
What Breaks Without Hooks
Helm hooks use annotations like helm.sh/hook: pre-upgrade to control Job execution order. Typical patterns include running DB migrations before the application deploys, or running a cache-warming Job after deployment completes.
Kustomize has no built-in hook mechanism to guarantee resource apply ordering. This has been discussed for a long time in GitHub Issue #1580, but there is no native support.
If You Use ArgoCD — Sync Waves
ArgoCD ignores the test and rollback Helm hooks. In sync-based deployments (the default), Sync Waves combinations effectively become the standard choice. However, if you want to preserve Helm hooks themselves, there is a workaround option to configure the application to deploy via helm upgrade — so don't take it as 100% mandatory.
# DB migration Job
apiVersion: batch/v1
kind: Job
metadata:
name: db-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/sync-wave: "1"
spec:
template:
spec:
containers:
- name: migrate
image: myapp:latest
command: ["./migrate"]
restartPolicy: Neverargocd.argoproj.io/hook: PreSync corresponds to Helm's pre-upgrade, and the sync-wave number controls execution order. PostSync corresponds to post-install and post-upgrade.
If You Use FluxCD — dependsOn
One thing worth clarifying here: if you keep a HelmRelease in Flux, the Helm SDK still executes hooks. But the moment you choose to replace with Kustomize as this post assumes — that is, the moment you migrate to a Kustomization resource — Helm hooks no longer work. If you want to keep Helm hooks, it's better to leave those releases out of the migration scope and keep them as HelmRelease.
For things you've migrated to Kustomize that need ordering, use dependsOn between Kustomization objects.
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: db-migrate
namespace: flux-system
spec:
dependsOn:
- name: database
path: ./jobs/migrateWithout GitOps — Two Pipeline Stages
If you're handling things directly in a CI/CD pipeline without GitOps tooling, you can guarantee ordering simply with a script.
# Step 1: Migration first
kubectl apply -k overlays/pre-install/
kubectl wait --for=condition=complete job/db-migrate --timeout=300s
# Step 2: Main application
kubectl apply -k overlays/production/Simple, but explicit. Instead of the magic of a hook mechanism, the order is visible in code — which is actually easier to trace in many cases.
Gap 3 — helm test-Based Validation
What helm test Does
It's a command that runs Pods/Jobs annotated with helm.sh/hook: test directly inside the cluster after a release and collects results. It's useful because you can verify from within the cluster whether service endpoints actually respond, whether DB connections work, and so on.
Kustomize has no single equivalent command.
Setting Up a Dedicated Test Overlay
Separating test resources by directory is the most intuitive approach.
k8s/
├── base/
│ └── deployment.yaml
├── overlays/
│ └── production/
└── tests/
├── kustomization.yaml
├── smoke-test-job.yaml
└── connectivity-test.yaml# tests/smoke-test-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
spec:
template:
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl
command: ["curl", "-f", "http://my-app/health"]# Run from CI pipeline
kubectl apply -k tests/
kubectl wait --for=condition=complete job/smoke-test --timeout=120s
kubectl logs job/smoke-test
kubectl delete -k tests/You can also reuse existing Helm test Job YAML — just remove the helm.sh/hook annotations and move them to the tests/ directory. The resource becomes directly runnable with kubectl. It's not much work and quite practical.
Combining Static Validation Tools
helm test was runtime validation, but you can actually build pre-deployment static validation that's richer than before.
| Tool | Role | Helm Equivalent |
|---|---|---|
| kubeconform | Kubernetes schema validation | Replaces helm lint |
| kube-score | Best practices quality checks | Additional validation |
| conftest (OPA/Rego) | Policy-based manifest testing | Covers helm test validity checking |
| Pluto | Deprecated API version detection | Additional safety net |
Trade-offs — An Honest Look
| Item | Improves After Kustomize Migration | Watch Out For |
|---|---|---|
| Readability | Pure Kubernetes manifests, IDE autocompletion fully works | - |
| Auditability | Instantly see final state with kubectl kustomize overlays/prod |
- |
| GitOps integration | Native integration with ArgoCD and FluxCD is simpler | Sync-based ArgoCD ignores test and rollback hooks |
| Hook management | - | Lifecycle management must be implemented manually via GitOps tools or scripts |
| Dynamic values | - | External tools needed for dynamic secret generation (recommended even with Helm) |
| Packaging | - | Fundamentally unsuitable for scenarios that package and distribute as a single chart |
| Conditional resources | - | Components handle it, but more granular splits increase reference relationships |
Honestly, the absence of hooks isn't "complexity disappearing" — it's "complexity moving." The execution order logic hidden inside Helm charts moves out into GitOps tool configuration or pipeline scripts. It's a question of where the complexity is more explicit.
Reasons to Defer Migration — The Helm 4 Variable
A quick aside — one variable to factor in right now. According to Atmosly's Helm 4 summary, Helm 4 has overhauled the Sprig execution engine and the hook and template rendering stream. The performance figures are from that blog's own measurements, so treat them as reference rather than grounds for a production migration decision. Verify against official release notes and your own benchmarks before concluding.
The key point is that you need to decide whether to push through Kustomize migration this quarter or re-evaluate after Helm 4 adoption. If you're considering a hybrid setup using the helmCharts field, these changes could have a meaningful impact.
Where to Start
One pattern has established itself as the most realistic approach as of 2026: migrate team-owned applications to Kustomize, and keep third-party charts (monitoring, ingress controllers, etc.) in Helm.
A suggested migration order:
-
Secret strategy first — Decide between External Secrets Operator and Sealed Secrets. As mentioned, this cleanup is necessary even if you keep using Helm, so bundling it with your migration plan strengthens the case for both.
-
Audit hook dependencies — List what your current charts'
pre-upgradeandpost-installhooks actually do. Whether it's DB migration or cache warming determines whether you go with ArgoCD Sync Waves, FluxdependsOn, or pipeline scripts. -
Redesign conditional resources as Components — Count your
if .Values.xxx.enabledblocks and slice them into components by feature unit. Design the structure at this stage to prevent overlay hierarchies from going too deep — it pays off in maintainability later. -
Build a test overlay — Take existing
helm testJob YAML, remove thehelm.sh/hookannotations, and move them to atests/directory. Adding an apply–wait–logs–delete loop to your CI pipeline isn't hard.
Closing Thoughts
A Kustomize migration is less "a path to simplicity" and more "a decision to move complexity elsewhere." The three gaps covered here — template functions, hooks, and testing — none of them are filled for free. Each one demands a cost: external secret tooling, GitOps tool configuration (or pipeline code), and a separate test overlay.
Whether that cost is worth bearing depends on your team's situation. But before making the decision, I'd recommend sketching out on paper exactly how you'll fill each of the three gaps. If there's a gap you can't picture filling, that's a signal to defer the migration.
References
- Helm vs Kustomize: We Manage 100+ Clusters - Here's What We Actually Use (2026) | Tasrie IT Services
- Helm vs Kustomize in 2025: Patterns, Pros, Cons, and How to Combine Them | justinpolidori.com
- pre-upgrade Hook · Issue #1580 · kubernetes-sigs/kustomize (GitHub)
- How to Handle Helm Chart Hooks vs ArgoCD Hooks Conflict
- Flux CD vs ArgoCD: Helm Support Comparison
- Helm Hooks and Chart Tests: Lifecycle Management Done Right | DevOpsil
- Replacing Helm and Kustomize with KRM Functions | Medium
- Power Up Helm Charts: Using Kustomize to Manage Kubernetes Deployments | JFrog Blog
- When and How to Use Helm and Kustomize Together | Thomas Stringer
- Helm 4 Migration Guide: What's New | Atmosly Blog
- Patch Any Helm Chart Template Using A Kustomize Post-Renderer | Austin Dewey
- Extending Kustomize | SIG CLI Official Docs
- The Kustomization File | SIG CLI Official Docs
- Kustomize Components | SIG CLI Official Docs
- khelm — Helm chart templating CLI / Kustomize plugin (GitHub)
- template-transformer — Kustomize transformer plugin (GitHub)