Stop copy-pasting YAML. Package, template, version, and parametrise the whole app.
8 YAML files with replicas and image tags baked in. Different envs would need forks of every file.
One chart, three values files (dev / stage / prod), helm upgrade per env, version history, and atomic rollback.
“Raw YAML scales to one env. Helm is what you use for two or more.”
helm history reads those Secrets.helm/
Chart.yaml # metadata: name, version, appVersion
values.yaml # default values
values-dev.yaml # dev overrides (1 replica, kind cluster)
values-stage.yaml # stage overrides
values-prod.yaml # prod overrides (3 replicas, real ingress)
templates/
_helpers.tpl # named-template helpers
api-deployment.yaml
api-service.yaml
worker-deployment.yaml
postgres-statefulset.yaml
configmap.yaml
secret.yaml # only when .Values.secrets.inChart=true
NOTES.txt # post-install message
Every file in templates/ is a Go template rendered against .Values, .Release, and .Chart.
image: {{ .Values.api.image }}:{{ .Values.api.tag }}
replicas: {{ .Values.api.replicas | default 2 }}
{{- if .Values.api.hpa.enabled }}
...
{{- end }}
{{- include "shortener.labels" . | nindent 4 }}
values.yaml designimage:
repository: ghcr.io/REPLACE_ME
pullPolicy: IfNotPresent
api:
replicas: 2
tag: "0.4.0"
probes:
enabled: true
worker:
replicas: 1
tag: "0.4.0"
postgres:
enabled: true # set false in stage/prod (managed DB)
storage: 1Gi
ingress:
enabled: false # turned on in L11
Top-level keys are component-shaped (api, worker, postgres, ingress); each component owns its own knobs.
# values-dev.yaml
api: {replicas: 1}
worker: {replicas: 1}
postgres: {enabled: true}
# values-prod.yaml
api: {replicas: 3, tag: "0.4.5"}
worker: {replicas: 2, tag: "0.4.5"}
postgres: {enabled: false} # managed RDS
ingress: {enabled: true, host: "s.example.com"}
helm upgrade --install shortener . -f values.yaml -f values-prod.yaml — last file wins on conflicts.
helm lint sample-app/helm/
helm template sample-app/helm/ -f values-dev.yaml | kubectl apply --dry-run=client -f -
helm install shortener sample-app/helm/ -f values-dev.yaml --dry-run --debug
Three commands cover schema validation, manifest rendering, and pre-flight without touching the cluster.
helm upgrade --install is your friendThe idempotent install-or-upgrade command. Pair it with --atomic so a failed upgrade rolls back automatically. Pair it with --wait so the command blocks until all pods are Ready. The canonical production deploy:
helm upgrade --install shortener . \
-f values-stage.yaml \
--atomic --wait --timeout 5m \
--namespace shortener --create-namespace
No separate helm install on first run; no manual rollback step on failure — one command for every deploy.
helm history shortener -n shortener
helm rollback shortener 3 -n shortener
Revision numbers track every upgrade; rolling back is a single command and works even mid-incident. The rollback itself becomes a new revision — your history is always append-only.
ct lint && ct install (from helm/chart-testing) runs helm lint, schema validation, and an actual install against a kind cluster spun up in the workflow. The GitHub Actions snippet that wires it up:
- uses: helm/kind-action@v1
- uses: helm/chart-testing-action@v2
- run: ct lint --chart-dirs sample-app --charts sample-app/helm
- run: ct install --chart-dirs sample-app --charts sample-app/helm
Pin chart-testing in CI to gate every PR that touches helm/.
sample-app/helm/.Chart.yaml, values.yaml, and 3 env overrides (values-dev, values-stage, values-prod)._helpers.tpl for the shared labels block.sample-app/k8s/ — the chart replaces it..github/workflows/chart-testing.yml that runs ct lint + ct install on PRs touching helm/.--atomic upgrades: from 8 fork-prone YAML files to a single source of truth.helm/ gets a real install on kind before merge.helm history is your rollback: one command, mid-incident, no scripts required.