import ComparisonTable from ’../../components/ComparisonTable.astro’;
Helm and Kustomize are the two dominant approaches to managing Kubernetes configuration. They take philosophically different approaches — Helm uses templating and packaging; Kustomize uses patch-based overlays on plain YAML.
Quick Verdict
Choose Helm if: You’re deploying complex applications with many configuration options, want to reuse community charts (PostgreSQL, Redis, etc.), or need release management (rollback, history).
Choose Kustomize if: You prefer plain YAML without templating logic, work in a GitOps workflow with ArgoCD/Flux, or are managing configuration variants (dev/staging/prod) of your own apps.
Architecture Comparison
<ComparisonTable headers={[“Dimension”, “Helm”, “Kustomize”]} rows={[ [“Approach”, “Go templates + values”, “Patch-based overlays”], [“Config format”, “Template YAML ({{ }})”, “Plain YAML”], [“Packaging”, “Charts (tar.gz)”, “Directory structure”], [“Distribution”, “Chart repositories”, “Git repos or OCI”], [“Release tracking”, “Built-in (helm list)”, “External (kubectl/ArgoCD)”], [“Rollback”, “helm rollback”, “Git revert”], [“Community ecosystem”, “Huge (ArtifactHub)”, “Limited”], [“Built into kubectl”, “No (separate CLI)”, “Yes (kubectl apply -k)”], [“GitOps friendly”, “Yes (with ArgoCD/Flux)”, “Native fit”], [“Complexity”, “Higher”, “Lower”], [“Secrets management”, “Plugin-based”, “External required”], ]} />
Helm Fundamentals
Helm uses Go templating to generate Kubernetes manifests from a chart:
Chart structure:
my-app/
├── Chart.yaml # Chart metadata
├── values.yaml # Default values
├── values-prod.yaml # Production overrides
├── templates/
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── _helpers.tpl # Template helpers
│ └── NOTES.txt # Post-install notes
└── charts/ # Chart dependencies
Chart.yaml:
apiVersion: v2
name: my-app
description: My application Helm chart
type: application
version: 1.2.0 # Chart version
appVersion: "2.4.1" # App version being packaged
values.yaml (defaults):
replicaCount: 1
image:
repository: ghcr.io/myorg/my-app
tag: "" # Defaults to Chart.appVersion
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: nginx
host: ""
tls: false
resources:
limits:
cpu: 500m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
env:
DATABASE_URL: ""
LOG_LEVEL: "info"
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilizationPercentage: 70
templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-app.fullname" . }}
labels:
{{- include "my-app.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- containerPort: 8080
env:
{{- range $key, $val := .Values.env }}
- name: {{ $key }}
value: {{ $val | quote }}
{{- end }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
Deploying with Helm:
# Install chart
helm install my-release ./my-app \
--namespace production \
--create-namespace \
--set image.tag=v2.4.1 \
--set ingress.enabled=true \
--set ingress.host=myapp.example.com \
-f values-prod.yaml
# List releases
helm list -n production
# Upgrade
helm upgrade my-release ./my-app \
--namespace production \
--set image.tag=v2.5.0
# Rollback to previous version
helm rollback my-release 1 -n production
# Show rendered templates (dry run)
helm template my-release ./my-app --debug
# Uninstall
helm uninstall my-release -n production
Using community charts:
# Add chart repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
# Install PostgreSQL (no chart authoring needed)
helm install my-postgres bitnami/postgresql \
--set auth.postgresPassword=secretpassword \
--set primary.persistence.size=20Gi \
--namespace databases
# Install Redis
helm install my-redis bitnami/redis \
--set auth.enabled=false \
--namespace databases
Kustomize Fundamentals
Kustomize works by defining a base configuration and then applying patches per environment:
Directory structure:
k8s/
├── base/
│ ├── kustomization.yaml
│ ├── deployment.yaml
│ ├── service.yaml
│ └── configmap.yaml
└── overlays/
├── staging/
│ ├── kustomization.yaml
│ └── replica-patch.yaml
└── production/
├── kustomization.yaml
├── replica-patch.yaml
└── ingress.yaml
base/deployment.yaml (plain YAML, no templating):
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: ghcr.io/myorg/my-app:latest
ports:
- containerPort: 8080
resources:
limits:
cpu: 200m
memory: 128Mi
base/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
commonLabels:
app: my-app
managed-by: kustomize
images:
- name: ghcr.io/myorg/my-app
newTag: latest # Override at overlay level
overlays/production/kustomization.yaml:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base
# Override image tag
images:
- name: ghcr.io/myorg/my-app
newTag: "2.4.1"
# Add namespace
namespace: production
# Apply patches
patches:
- path: replica-patch.yaml
- path: resource-patch.yaml
# Add resources not in base
resources:
- ingress.yaml
# Add common labels for production
commonLabels:
environment: production
overlays/production/replica-patch.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # Override base (1 → 3)
Deploying with Kustomize:
# Preview what will be applied
kubectl kustomize overlays/production
# Apply production
kubectl apply -k overlays/production
# Apply staging
kubectl apply -k overlays/staging
# Delete
kubectl delete -k overlays/production
Comparison: Same App, Different Tools
Helm (values approach):
# Prod
helm install my-app ./chart -f values-prod.yaml
# Staging
helm install my-app ./chart -f values-staging.yaml
# The difference lives in values files (key-value pairs)
Kustomize (patch approach):
# Prod
kubectl apply -k overlays/production
# Staging
kubectl apply -k overlays/staging
# The difference lives in patch files (valid YAML fragments)
Kustomize patches are valid YAML that is merged — easier to review in PRs than template files.
GitOps with ArgoCD
Both work with ArgoCD, but Kustomize integrates more naturally:
ArgoCD with Kustomize:
# ArgoCD Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-production
spec:
source:
repoURL: https://github.com/myorg/infra
path: k8s/overlays/production # Just point to directory
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
ArgoCD with Helm:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app-production
spec:
source:
repoURL: https://github.com/myorg/infra
path: helm/my-app
targetRevision: main
helm:
valueFiles:
- values-production.yaml
parameters:
- name: image.tag
value: "2.4.1"
Using Both Together
A common pattern: use Helm to deploy third-party apps, Kustomize to patch them:
# kustomization.yaml — patch a Helm-deployed chart
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Post-render hook in Helm (pipe Helm output through Kustomize)
resources: []
patches:
- target:
kind: Deployment
name: nginx-ingress-controller
patch: |
- op: replace
path: /spec/replicas
value: 3
ArgoCD supports Helm + Kustomize post-rendering natively.
When to Choose Each
Choose Helm:
- Installing third-party apps (PostgreSQL, Prometheus, cert-manager)
- Distributing your own app as a reusable chart
- Teams who want values files over patch files
- Need release history and rollback commands
- Complex conditional logic in configuration
Choose Kustomize:
- Pure GitOps workflow with ArgoCD or Flux
- Prefer plain YAML that’s easy to diff and review
- Managing per-environment overlays for your own apps
- Want zero templating logic in configuration files
- Built into kubectl (no extra CLI to install)
Bottom Line
Helm and Kustomize are complementary, not competing: use Helm to install community-maintained charts (it’s the only practical way to get PostgreSQL or cert-manager), and use Kustomize to manage environment-specific configuration for your own applications. Teams shouldn’t feel forced to pick one — the most effective setup uses both where each shines.