Helm vs Kustomize: Templating Trade-offs at Scale
Helm vs Kustomize
Helm is a package manager. It templates YAML with Go templates, manages releases with versioning, and handles dependencies between charts. Kustomize is a configuration customization tool. It takes existing YAML and patches it for different environments without templating.
The decision is not Helm or Kustomize. The decision is: does this workload need templating (parameterization for multiple consumers) or patching (customization for multiple environments)?
The Failure
The platform team created a Helm chart for every service. Each chart had 400 lines of Go templates with {{ if }} blocks, .Values references, and helper templates. When a developer needed to add a sidecar container, they had to learn Go template syntax, understand the chart’s value structure, and modify templates that affected every team. A one-line Kubernetes change became a three-day PR.
Kustomize would have been simpler. A sidecar is a strategic merge patch: add the container to the patch file, commit, done. No templating language. No value hierarchy. No risk of breaking other teams’ configurations.
The Mechanism
When to Use Which
| Scenario | Tool | Reason |
|---|---|---|
| Third-party software (Prometheus, Nginx, Redis) | Helm | Charts maintained by vendor, values-based config |
| Internal microservice | Kustomize | Simple patches per environment, no templating needed |
| Shared internal library chart | Helm | Reusable across teams with different values |
| Platform add-ons (cert-manager, external-dns) | Helm | Community charts with well-tested defaults |
| Environment-specific overrides | Kustomize | Overlays for dev/staging/production |
| Multi-team shared infrastructure | Helm + Kustomize | Helm for base chart, Kustomize for team-specific patches |
The Decision Rule
If you are consuming someone else’s configuration → Helm (use their chart, override values). If you are customizing your own configuration → Kustomize (patch your base for each environment). If you are publishing configuration for others → Helm (package as a chart with values).
The Implementation
Kustomize for Internal Services
# apps/checkout-service/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- hpa.yaml
commonLabels:
app.kubernetes.io/name: checkout-service
app.kubernetes.io/part-of: ecommerce
# apps/checkout-service/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
patches:
- path: patch-replicas.yaml
- path: patch-resources.yaml
images:
- name: ghcr.io/acme/checkout-service
newTag: abc123
Helm for Third-Party Software
# platform/prometheus/values-production.yaml
# HARDENED: Prometheus values for production
prometheus:
prometheusSpec:
replicas: 2
retention: 30d
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: 2000m
memory: 4Gi
storageSpec:
volumeClaimTemplate:
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 100Gi
Helm + Kustomize (Post-Rendering)
When a Helm chart does not expose a value you need, use Kustomize as a post-renderer:
# ArgoCD Application with Helm + Kustomize post-rendering
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: prometheus
namespace: argocd
spec:
source:
repoURL: https://prometheus-community.github.io/helm-charts
chart: kube-prometheus-stack
targetRevision: 56.6.2
helm:
valueFiles:
- values-production.yaml
# Kustomize patches applied AFTER Helm rendering
# Useful for adding annotations, labels, or sidecars
# that the Helm chart doesn't expose as values
The Gate
For Kustomize, the gate is kustomize build in CI. If the build fails, the manifests are invalid. Run kustomize build for every overlay and pipe the output to kubectl apply --dry-run=server to validate against the cluster’s API.
For Helm, the gate is helm template + helm lint. Template rendering catches value errors. Lint catches chart structure issues.
# CI validation for both approaches
- name: Validate Kustomize overlays
run: |
for overlay in apps/*/overlays/*/; do
echo "Validating $overlay..."
kustomize build "$overlay" | kubectl apply --dry-run=server -f -
done
- name: Validate Helm charts
run: |
for chart in platform/*/; do
echo "Linting $chart..."
helm lint "$chart"
helm template "$chart" -f "$chart/values-production.yaml" | kubectl apply --dry-run=server -f -
done
The Recovery
Kustomize patch does not apply cleanly: The base resource structure changed and the strategic merge patch no longer matches. Use kubectl diff to see what the patch produces. Update the patch to match the new base structure.
Helm chart upgrade breaks values: Pin the chart version in the ArgoCD Application targetRevision. Upgrade chart versions deliberately, not automatically. Read the chart’s changelog before upgrading.
Team outgrew Kustomize: If a Kustomize base has more than 5 overlays that all patch the same fields differently, consider converting to a Helm chart with values. The threshold is when patches become harder to maintain than templates.