Templating vs Overlaying Manifests
Summary
- Templating and overlaying are the two dominant ways to turn one set of Kubernetes manifests into per-environment variants.
- Insight: Templating treats YAML as text to be generated; overlaying treats YAML as structured data to be patched.
- Key Concepts: Go text/template (Helm), values injection, strategic-merge and JSON patches (Kustomize), base, overlay, render step.
- When to Use: Reach for templating when you ship a chart to others; reach for overlaying when you own the manifests and want minimal per-env diffs.
- Limitations/Trade-offs: Templates are flexible but can drift into unreadable logic; overlays are transparent but weaker at wide parameterization and packaging.
- Related Topics: Helm charts, Kustomize bases/overlays, GitOps promotion, ADR-style tool selection.
Foundations
Both approaches solve the same problem: you have a Deployment, Service, and ConfigMap that are nearly identical across dev, staging, and production, and you do not want three hand-maintained copies.
The difference is where the variation lives and how it is applied.
Templating (Helm's model) generates final YAML from parameterized text. You write a template with placeholders, supply a values.yaml, and a render engine produces the manifests.
Overlaying (Kustomize's model) starts from real, valid YAML - the base - and applies declarative patches on top to produce each environment's output.
A useful mental model: templating is a function render(template, values) -> yaml, while overlaying is a fold patch(base, [patchA, patchB]) -> yaml.
Templating can express things overlays cannot easily express, such as generating a variable number of objects from a list.
Overlaying can never produce invalid intermediate YAML, because every layer is itself parseable Kubernetes YAML.
Mechanics & Interactions
Helm uses Go's text/template engine plus the Sprig function library. A chart directory holds templates/, a values.yaml, and Chart.yaml.
Placeholders look like {{ .Values.replicaCount }}, and control flow like {{- if .Values.ingress.enabled }} lets you include or omit whole objects.
At install time Helm renders the templates, merges the result, and sends the objects to the API server as a tracked release.
# templates/deployment.yaml (Helm)
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "app.fullname" . }}
spec:
replicas: {{ .Values.replicaCount }}
template:
spec:
containers:
- name: app
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"Kustomize walks a kustomization.yaml, loads the referenced resources, then applies transformers and patches. There is no text substitution and no templating language.
# overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
replicas:
- name: app
count: 5
images:
- name: app
newTag: 1.8.3The base contains complete manifests; the overlay only records the delta for that environment.
Kustomize is built into kubectl (kubectl apply -k), and Helm is a separate binary that also talks to the cluster and stores release state as Secrets.
The two can interact: Kustomize can consume Helm-rendered output through its helmCharts field, letting you post-process a third-party chart with overlays.
Advanced Considerations & Applications
Templating shines for redistributable software. If you publish a database or an ingress controller, a chart with well-documented values is the expected delivery format.
Overlaying shines for your own application manifests in a GitOps repo, where reviewers want to see exactly what changes between staging and production.
Consider readability of diffs during review. An overlay pull request shows a two-line replica change; a Helm change may show only a values edit whose rendered impact is not visible without helm template.
Consider the failure mode of each. A template bug can emit malformed YAML that only fails at apply time; an overlay bug usually fails at build time because Kustomize validates structure.
Many teams run a hybrid: package with Helm for distribution, then feed the rendered chart into Kustomize for site-specific tweaks that the chart author never anticipated.
For GitOps, both render deterministically in CI. Argo CD and Flux natively support Helm charts and Kustomize directories, so either can be the source of truth.
Whichever you choose, keep the render step in CI. Running helm template or kustomize build and committing or diffing the output catches surprises before they reach the cluster.
Common Misconceptions
"Kustomize cannot do parameterization." It can vary images, replicas, names, labels, and arbitrary fields via patches; it just cannot inject free-form variables into arbitrary text.
"Helm requires Tiller and cluster-side components." That was Helm 2. Helm 3 is client-only and stores release state as Secrets in the target namespace.
"You must choose one tool forever." They compose. Kustomize can post-render Helm output, and Helm 3 supports post-renderers that call Kustomize.
"Templating is always more powerful, so always use Helm." Power is not free; heavy template logic is a common source of unreadable, hard-to-review charts.
"Overlays only patch fields." Overlays can also add resources, set common labels and annotations, generate ConfigMaps, and prefix names across every object.
FAQs
Which produces valid Kubernetes YAML at every step? Kustomize, because each layer is real YAML. Helm intermediate templates are text and only become valid after rendering.
Can I see the final output before applying? Yes. Use helm template ./chart -f values.yaml or kustomize build overlays/production.
Does Kustomize have variables? It has replacements and generators, but no general-purpose variable substitution like a templating language.
Which is better for a public open-source component? Helm, since charts are the conventional distribution format with discoverable values.
Which is better for internal app promotion across environments? Kustomize overlays usually give smaller, clearer per-environment diffs.
Can I use both together? Yes. A common pattern is Helm for packaging and Kustomize for last-mile, cluster-specific patches.
Related
Stack versions: This page was written for Kubernetes 1.36.2, Docker Engine 29.6.1 (BuildKit default), containerd (CRI runtime on nodes), Helm 3, Compose v2, Argo CD (latest - verify at build), and Gateway API (GA - verify controller support at build).