Helm fills in a chart from values.yaml. Kustomize patches YAML you can already read. Both emit manifests, and both can set the same replica count and image tag. This page shows that change both ways, then when a chart, an overlay, or both is the right owner.
Short answer
Choose Helm when the unit of delivery is a version: a tarball or an OCI artifact, a revision history, and one command that installs or rolls back every object. Choose Kustomize when the unit is a directory and each environment is a patch on a base. kubectl 1.30 already includes Kustomize, so kubectl apply -k, kubectl diff -k, and kubectl kustomize need no extra install.
Helm templates are Go templates. They can skip a document with if, or fail the render when a value is missing. Kustomize has no template language. An optional object is a file listed only in the overlay that needs it.
Side by side
| Helm 3 | Kustomize | |
|---|---|---|
| Input | Chart (Chart.yaml, values.yaml, templates/) |
Directory with kustomization.yaml |
| How environments differ | Another values file, or --set |
Overlay: images, replicas, patches, namespace |
| Dry run | helm template, or helm upgrade --dry-run |
kubectl kustomize, or kubectl diff -k |
| Apply | helm upgrade --install |
kubectl apply -k |
| History | Revisions in a Secret in the release namespace | Git history, plus Deployment rollout history |
| Roll back the whole set | helm rollback <release> <revision> |
Apply an older commit |
| Roll back one Deployment | Possible, but the next upgrade may revert it | kubectl rollout undo |
| Share with another team | Chart repo or OCI registry | Copy a directory, or point at a remote base |
| Built into kubectl | No | Yes (-k / kubectl kustomize) |
kubectl diff -k exits 1 when the live objects differ from the build. That is a diff, not a broken binary. Account for it in CI before you treat the exit code as a failure.
The same change, both ways
The goal is three API replicas and the tag 1.4.2.
Helm values:
replicaCount: 3
image:
repository: example/api
tag: "1.4.2"
The template only replaces what varies. required stops a render that would otherwise print an empty replica count:
spec:
replicas: {{ required "replicaCount is required" .Values.replicaCount }}
template:
spec:
containers:
- name: api
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
helm lint ./chart
helm upgrade --install api ./chart -n demo --create-namespace -f values/prod.yaml --wait --timeout 3m
helm history api -n demo
--wait blocks until the Deployment is available or --timeout hits. --atomic rolls a failed upgrade back, and uninstalls a failed first install.
Kustomize base lists the plain manifests. The production overlay does not copy them:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: prod
replicas:
- name: api
count: 3
images:
- name: example/api
newTag: "1.4.2"
patches:
- path: memory.yaml
target:
kind: Deployment
name: api
newTag edits YAML. It does not build an image. images.name is the repository without the tag. In the patch below, the container name is the merge key:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
template:
spec:
containers:
- name: api
resources:
requests:
memory: 256Mi
limits:
memory: 512Mi
kubectl kustomize overlays/prod
kubectl diff -k overlays/prod
kubectl apply -k overlays/prod
-k is the built-in flag on apply, diff, and delete. kubectl kustomize only prints the build.
Prefer labels when you do not want selectors rewritten. commonLabels still writes onto selectors, which are painful to change later.
labels:
- pairs:
app.kubernetes.io/part-of: shop
includeSelectors: false
When to pick Helm
Pick Helm for ingress-nginx, cert-manager, or any chart you do not want to fork. Pin a version. HTTP repositories still work:
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm search repo ingress-nginx/ingress-nginx --versions
helm show values ingress-nginx/ingress-nginx
helm show values prints the keys the chart honors. If a setting is missing, patch the render later. Do not fork a chart for a one-line memory limit.
Publish your own app as an OCI artifact. The push URL has no chart name and no version. Helm reads both from the tarball. The install URL includes the chart name.
helm package ./chart
helm registry login ghcr.io -u myuser
helm push shop-0.1.0.tgz oci://ghcr.io/example/charts
helm upgrade --install shop oci://ghcr.io/example/charts/shop --version 0.1.0 -n demo --create-namespace
helm package names the file from Chart.yaml (name plus version). apiVersion: v2 is the Helm 3 chart API. helm registry login takes a host (ghcr.io), not a URL with a scheme or a path.
Use a template if when a document exists only in some environments. Keep the template thin so reviewers can still see the YAML.
When to pick Kustomize
Pick Kustomize when every environment is the same Deployment with a different tag, namespace, or replica count. The base stays valid YAML. Overlays do not loop. The git diff is the cluster diff.
configMapGenerator and secretGenerator hash the object name and rewrite references in that kustomization, so a data change rolls Pods. Set generatorOptions.disableNameSuffixHash: true only when a caller outside the overlay needs a fixed name. You then own the rollout.
Try the overlay on a laptop cluster from kind vs minikube vs k3d before you point it at a shared API. The app-shaped Compose-to-manifest move is covered in Docker Compose vs Kubernetes.
When to use both
One owner. A later helm upgrade overwrites a field you patched with kubectl. If you apply rendered YAML and never install a release, helm rollback has nothing to restore.
Render, then patch. This is the workable form of “helm template, then kustomize”. kubectl apply -k reads a directory, not a pipe, so write the render to a file the overlay lists:
helm template shop ./chart -n prod -f values/prod.yaml > overlays/prod/all.yaml
kubectl apply -k overlays/prod
Roll back by applying an older all.yaml, or kubectl rollout undo deployment/api -n prod for that Deployment only. More kubectl patterns are in the kubectl cheat sheet.
Inflate a chart inside Kustomize. helmCharts runs helm template as a subprocess. Helm must be on PATH (--helm-command if the binary is named something else). --enable-helm exists on kubectl kustomize and does not exist on kubectl apply, so -k errors when helmCharts is present.
helmCharts:
- name: ingress-nginx
repo: https://kubernetes.github.io/ingress-nginx
version: X.Y.Z
releaseName: ingress-nginx
namespace: ingress-nginx
valuesFile: values.yaml
Replace X.Y.Z with a version from helm search repo ingress-nginx/ingress-nginx --versions. Then:
kubectl kustomize --enable-helm overlays/prod | kubectl apply -f -
There is still no Helm release. The chart is only a source of YAML.
Secrets
Helm stores each revision in a Secret named sh.helm.release.v1.<release>.v<revision> (type helm.sh/release.v1) in the release namespace. The payload includes the values. Anyone who can get Secrets there can recover them. Encryption at rest is an etcd setting, not a Helm flag.
Point the chart at a Secret that already exists (existingSecret is a common values key, not a universal field). Create it outside git:
kubectl create secret generic app-secret -n demo --from-literal=POSTGRES_PASSWORD="$POSTGRES_PASSWORD"
A secretGenerator literal is plaintext in git. kubectl apply -k creates the Secret and does not decrypt anything. Decrypt before apply if you need that. Exec generators are not part of a plain -k run.
helm.sh/resource-policy: keep tells helm uninstall to leave that object. It does not hide or encrypt it.
Migration path
Helm release to Kustomize. Capture the render, mark objects so uninstall leaves them, then apply the directory.
helm get manifest api -n demo > base/all.yaml
helm get values api -n demo > values-used.yaml
kubectl -n demo annotate deploy,svc,cm,secret,pvc -l app.kubernetes.io/instance=api helm.sh/resource-policy=keep --overwrite
helm uninstall api -n demo
kubectl apply -k overlays/prod
Helm 3 sets app.kubernetes.io/instance to the release name unless the chart replaced labels. If one type has no matches, drop it and annotate again. Uninstall deletes anything without helm.sh/resource-policy: keep. Remove the annotation later with a trailing dash: kubectl annotate ... helm.sh/resource-policy-.
Kustomize directory to a Helm release. Diff first. Helm 3.2 and later refuses existing objects until they carry its ownership metadata.
kubectl kustomize overlays/prod > /tmp/before.yaml
helm template api ./chart -n demo -f values/prod.yaml > /tmp/after.yaml
diff -u /tmp/before.yaml /tmp/after.yaml
When the remaining diff is only labels you accept, mark the objects and install:
kubectl -n demo annotate deployment/api svc/api cm/app-config secret/app-secret \
meta.helm.sh/release-name=api meta.helm.sh/release-namespace=demo --overwrite
kubectl -n demo label deployment/api svc/api cm/app-config secret/app-secret \
app.kubernetes.io/managed-by=Helm --overwrite
helm upgrade --install api ./chart -n demo -f values/prod.yaml
Repeat that for every object the chart renders.
helm rollback api 2 -n demo --wait
Omit the revision to return to the previous one. kubectl rollout undo deployment/api -n demo only rewinds that Deployment’s Pod template. Use helm rollback when the bad change spanned the Ingress, the ConfigMap, and the Deployment together.
FAQ
Should I use Helm or Kustomize?
Use Helm when you publish a versioned package or install someone else’s chart. Use Kustomize when you own the YAML and the environments differ by image tag, replica count, namespace, or a small patch. kubectl already runs Kustomize. Helm is a separate binary.
Can I use Helm and Kustomize together?
Yes, if only one of them owns the apply. Render with helm template, patch with Kustomize, and kubectl apply -k the result. Or render a chart with kubectl kustomize --enable-helm and apply that output. Do not helm upgrade objects that Kustomize also patches. The next upgrade puts the chart’s copy back.
How do I roll back a Helm release versus a Kustomize apply?
helm rollback <release> <revision> restores every object from that revision. Kustomize has no release object. Apply an older overlay from git, or run kubectl rollout undo deployment/<name> for one Deployment. The undo does not revert Services, ConfigMaps, or anything else in the overlay.
Where should database passwords live?
In a Secret created outside the git repo, referenced by name. Helm copies values into a release Secret in the same namespace, so a password in values.yaml is readable by anyone who can get Secrets. Kustomize secretGenerator literals are plaintext in git. Neither tool is a secret store.
How this guide was verified
Written by hand from the official documentation, every command re-checked, and reviewed a second time before publishing. Last reviewed on .
Official sources read for this article

