Skip to main content
ship it and sleep

Helm vs Kustomize: Templating Trade-offs at Scale

4 min read Chapter 37 of 66

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)?

Helm vs Kustomize decision tree

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

ScenarioToolReason
Third-party software (Prometheus, Nginx, Redis)HelmCharts maintained by vendor, values-based config
Internal microserviceKustomizeSimple patches per environment, no templating needed
Shared internal library chartHelmReusable across teams with different values
Platform add-ons (cert-manager, external-dns)HelmCommunity charts with well-tested defaults
Environment-specific overridesKustomizeOverlays for dev/staging/production
Multi-team shared infrastructureHelm + KustomizeHelm 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.