Most teams that use Kustomize for their own manifests still need a few third-party charts, such as an ingress controller, a metrics exporter or a database operator. Kustomize can render those charts itself through the helmCharts field, so the chart output goes through the same overlays, patches and labels as everything else. This guide walks through a working setup, explains where Kustomize stores the chart and how values are combined, and covers the namespace behavior that changed in Kustomize v5.8 and still catches people out. The details come from the Kustomize v5.8.3 source, which is the current release.
What helmCharts actually does
Kustomize doesn't reimplement Helm. For each helmCharts entry it runs the helm binary as a subprocess, first helm pull if the chart isn't on disk yet, then helm template. The rendered YAML becomes regular resources in the build, and every transformer that runs after it (patches, labels, images, name prefixes) can change it.
Because the chart is only templated, Helm never installs a release. There is no release Secret in the cluster, helm list shows nothing and helm rollback has nothing to roll back. You get Helm's templating and Kustomize's lifecycle, which is usually what you want when Git is the source of truth. Hooks are rendered as ordinary resources unless you set skipHooks: true, so read what a chart puts in its hooks before you rely on this.
Step 1: install Helm and enable the generator
Kustomize v5.8.1 and later accept Helm v3 or v4. Before rendering anything, Kustomize runs helm version --short and stops if the major version is anything else. It looks for a binary named helm on your PATH, and you can point it somewhere else with --helm-command.
The generator is off by default. You have to pass --enable-helm on every build.
helm version --short
kustomize build --enable-helm .
kubectl kustomize --enable-helm .
kubectl kustomize accepts the same flags as the standalone binary. kubectl apply -k does not. It builds with the default options, which leave the Helm generator disabled, so a kustomization with helmCharts fails with an error that mentions must specify --enable-helm. Render first and pipe the result to kubectl apply.
kustomize build --enable-helm overlays/prod | kubectl apply -f -
Step 2: add the chart to a base
Put the chart in a base kustomization, so every environment renders the same chart version and overlays only hold the differences. This example uses the podinfo chart, a small demo web service that's handy for trying things out.
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
helmCharts:
- name: podinfo
repo: https://stefanprodan.github.io/podinfo
version: 6.17.0
releaseName: podinfo
valuesInline:
replicaCount: 2
ui:
message: "Rendered by Kustomize"
Always set releaseName. Without it Kustomize passes --generate-name to helm template, and the Kustomize source itself notes that this doesn't work as intended. Most charts build object names from the release name, so a missing or changing release name means renamed objects on the next apply.
Pin version as well. Leaving it out means Helm pulls whatever is newest when the chart directory is empty, and two machines can end up rendering different charts from the same commit.
Charts published to an OCI registry work too. Put the registry path in repo with the oci:// scheme and keep the chart name in name. Kustomize joins the two when it pulls.
helmCharts:
- name: podinfo
repo: oci://ghcr.io/stefanprodan/charts
version: 6.17.0
releaseName: podinfo
Step 3: understand where the chart is stored
Kustomize keeps pulled charts in a directory called the chart home. The default is charts, relative to the kustomization that declares the chart, and you can change it with helmGlobals.chartHome.
When an entry sets both repo and version, Kustomize adds a versioned subdirectory. The podinfo chart above ends up in base/charts/podinfo-6.17.0/podinfo. If that directory exists at build time, Kustomize uses it as found and doesn't contact the repository. If it doesn't exist, Kustomize runs helm pull --untar into it.
That has two practical effects. Bumping version changes the directory name, so the next build pulls the new chart instead of quietly reusing the old one. And you get to choose between committing the charts directory or ignoring it. Committing it gives you offline, reproducible builds and lets reviewers see chart changes in diffs. Ignoring it keeps the repository small but makes every clean CI run download the chart. For production repositories we commit it, because a chart that disappears from its repository should not break a rollback.
Kustomize gives Helm a temporary config, cache and data directory for each run, through HELM_CONFIG_HOME, HELM_CACHE_HOME and HELM_DATA_HOME. Repositories you added with helm repo add on your laptop are not used. Set helmGlobals.configHome if you need Helm to see a specific config directory, for example one with registry credentials.
Step 4: pass values without surprises
There are three ways to set values, and they combine in a fixed order. valuesFile replaces the chart's own values.yaml as the starting point. valuesInline is then merged into that starting point. additionalValuesFiles are passed to Helm afterwards as extra -f flags.
The merge in the second step is controlled by valuesMerge. The default, override, lets inline values win over the values file. With merge, existing values in the file win and inline values only fill gaps. With replace, the inline values are used alone and the file is ignored. In every mode Helm still applies the chart's built-in defaults underneath.
The order of the third step matters more than people expect. Kustomize writes the merged result to a temporary file and passes it to Helm first, then each additional values file. Helm lets later files win, so a key in additionalValuesFiles beats the same key in valuesInline.
helmCharts:
- name: podinfo
repo: https://stefanprodan.github.io/podinfo
version: 6.17.0
releaseName: podinfo
valuesInline:
replicaCount: 2
additionalValuesFiles:
- values-prod.yaml
If values-prod.yaml sets replicaCount: 3, the Deployment gets 3 replicas. Keep each key in one place and this never comes up.
Values files must sit inside the kustomization's directory tree. They go through the same load restrictor as patches, so ../shared/values.yaml fails unless you build with --load-restrictor LoadRestrictionsNone. Copy shared values into each kustomization that needs them.
Two more fields solve common gaps. CRDs in a chart's crds/ directory are left out of helm template by default, so set includeCRDs: true when the chart is the thing that should install them. And because helm template has no cluster to ask, charts that check .Capabilities.APIVersions can skip resources, a ServiceMonitor for example. List the API versions the chart checks for in apiVersions, or pass --helm-api-versions on the command line. Set kubeVersion or --helm-kube-version for charts that branch on the cluster version. The command line flags override what the kustomization says.
Step 5: know how namespaces work since v5.8
This is the part that changed. Before v5.8.0, Kustomize ran helm template without a namespace (unless the chart entry set one) and then let its namespace transformer stamp the kustomization's namespace on every rendered resource. That put objects in the right namespace, but anything the chart computed from .Release.Namespace, such as a service URL in a ConfigMap or the subject of a RoleBinding, still said default. It also moved resources the chart deliberately placed in another namespace, like a RoleBinding in kube-system.
Kustomize v5.8.0 changed both halves, and v5.8.1 finished the fix for nested kustomizations. If a chart entry has no namespace of its own, Kustomize now passes the kustomization's namespace to Helm with --namespace, including a namespace set by an overlay that includes the base. Then the namespace transformer skips every resource the chart produced. It marks them with an internal annotation, internal.config.kubernetes.io/helm-generated, which it removes before printing the output.
Charts that write namespace: {{ .Release.Namespace }} in their templates, as podinfo does, now come out fully correct. Charts that leave metadata.namespace out of their templates come out with no namespace at all, because helm template doesn't add one and Kustomize no longer does either. That regression is tracked as issue #6058 in the Kustomize repository, which was still open when this was written. The maintainers have discussed an opt-in setting rather than a return to the old default.
Check for the problem before you upgrade. Build with the new version and list every object that has no namespace. This uses yq v4. Cluster-scoped kinds such as ClusterRole or CustomResourceDefinition show up too and are fine, so look for Deployments, Services, ConfigMaps and the like.
kustomize build --enable-helm overlays/prod \
| yq 'select(.metadata.namespace == null) | .kind + "/" + .metadata.name'
You have three ways out, from cleanest to most local.
First, check the chart's values. Many charts accept a namespaceOverride value or similar and put it into every template. That fixes the source, and it survives future Kustomize changes.
Second, let the apply step fill the gap. kubectl apply puts objects without a namespace into the namespace given by -n, and it refuses objects whose namespace doesn't match. Argo CD does the same with the destination namespace of an Application.
kustomize build --enable-helm overlays/prod | kubectl apply -n shop -f -
Third, patch the namespace in. The namespace transformer skips chart resources, but patches do not. A JSON 6902 patch can add metadata.namespace, and the patch target accepts a regular expression for the kind. Narrow it with the release label so you don't touch resources the chart placed in other namespaces on purpose. A strategic merge patch won't work here, because Kustomize restores the original namespace after applying one.
# overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: shop
resources:
- ../../base
patches:
- target:
kind: Deployment|Service|ServiceAccount|ConfigMap
labelSelector: app.kubernetes.io/instance=podinfo
patch: |-
- op: add
path: /metadata/namespace
value: shop
The add operation also replaces a value that's already there, so the patch is safe on resources that do have a namespace. Treat it as a stopgap and remove it once the chart or Kustomize gives you a proper fix.
When helmCharts is the wrong tool
The generator suits charts you consume and adjust a little. It's a poor fit when you need Helm's release features, such as hooks that run in order, helm rollback, or helm upgrade handling of resources a chart has removed. In that case install the chart with Helm or a GitOps controller that runs Helm, and use Kustomize only as a post-renderer if you need patches on top. If you mostly want reproducible YAML, a third option is to run helm template once, commit the output and treat it as a normal base. It's more files to review, but upgrades become a plain diff.