Helm — Advanced Patterns
Beyond `helm install`. Hooks, library charts, OCI registries, testing, and patterns for production-grade chart management.
Beyond helm install. Hooks, library charts, OCI registries, testing, and patterns for production-grade chart management.
Chart Structure Deep Dive
my-chart/
├── Chart.yaml # metadata: name, version, appVersion, dependencies
├── values.yaml # default values
├── values-prod.yaml # environment overrides (not shipped in chart)
├── templates/
│ ├── _helpers.tpl # named templates (reusable snippets)
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── configmap.yaml
│ ├── hpa.yaml
│ ├── serviceaccount.yaml
│ ├── NOTES.txt # printed after install
│ └── tests/
│ └── test-connection.yaml
└── charts/ # vendored dependency charts
Named Templates (_helpers.tpl)
{{/* Standard labels for all resources */}}
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ include "my-chart.chart" . }}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/* Selector labels */}}
{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}Usage in templates:
metadata:
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "my-chart.selectorLabels" . | nindent 6 }}Hooks
Run Jobs at specific points in the release lifecycle.
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ .Release.Name }}-db-migrate"
annotations:
"helm.sh/hook": pre-upgrade,pre-install
"helm.sh/hook-weight": "-5" # lower = runs first
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migrate
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ["python", "manage.py", "migrate"]
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-creds
key: urlHook annotations:
pre-install/post-install— before/after first installpre-upgrade/post-upgrade— before/after upgradespre-delete/post-delete— before/after deletionpre-rollback/post-rollback— before/after rollbacks
Chart Dependencies
# Chart.yaml
dependencies:
- name: postgresql
version: "14.3.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled # toggle via values
- name: redis
version: "18.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabledhelm dependency update my-chart # download deps to charts/
helm dependency build my-chart # use Chart.lock (pinned versions)# values.yaml — configure sub-charts under their chart name
postgresql:
enabled: true
auth:
postgresPassword: "{{ .Values.database.password }}"
primary:
persistence:
size: 50Gi
redis:
enabled: falseOCI Registry
Helm 3.8+ supports storing charts in OCI registries (ECR, GCR, GHCR, Docker Hub).
# Push chart to GHCR
helm package my-chart
helm push my-chart-1.2.3.tgz oci://ghcr.io/my-org/charts
# Install from OCI
helm install my-release oci://ghcr.io/my-org/charts/my-chart --version 1.2.3
# Authenticate
echo $GITHUB_TOKEN | helm registry login ghcr.io --username my-user --password-stdinOCI replaces HTTP chart repositories for private charts. No need to run a chart museum.
Helm Test
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ .Release.Name }}-test-connection"
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox
command: ['wget', '--no-verbose', '--tries=1', '--spider',
'http://{{ include "my-chart.fullname" . }}:{{ .Values.service.port }}/health']helm test my-release # runs test pods, reports pass/failDiff and Dry-Run
# Show what will change before upgrading
helm diff upgrade my-release my-chart -f values-prod.yaml
# Render templates without applying (debug)
helm template my-release my-chart -f values-prod.yaml
# Dry-run (server-side validation)
helm upgrade my-release my-chart -f values-prod.yaml --dry-runhelm-diff plugin is essential for code review of Helm changes — shows a kubectl-diff style output of every manifest change.
Common Failure Cases
Hook job from a previous release blocks the next upgrade
Why: helm.sh/hook-delete-policy was not set (or set to hook-succeeded only), so a failed hook Job remains and the new upgrade cannot create a job with the same name.
Detect: helm upgrade exits with Error: rendered manifests contain a resource that already exists; the old Job is visible in kubectl get jobs.
Fix: manually delete the stale Job, then re-run the upgrade; add before-hook-creation to the hook's delete policy to prevent recurrence.
helm dependency update pulls a different sub-chart version than expected
Why: Chart.yaml uses a loose version range (e.g., 14.3.x) and a new patch release broke a breaking change in the sub-chart API.
Detect: helm template or helm upgrade --dry-run fails with an unexpected field error originating from the sub-chart.
Fix: pin the dependency to the exact version in Chart.yaml and commit Chart.lock to source control so all environments use the identical chart.
OCI chart pull fails with "401 unauthorized" in CI
Why: the OCI registry authentication token was generated before the Helm client session started, or the credentials helper is not configured for the OCI URL prefix.
Detect: helm pull oci://... returns Error: unexpected status code 401; the equivalent docker pull succeeds.
Fix: run helm registry login <registry> explicitly before any OCI pull/push commands, using the same credential source as your Docker login.
helm rollback does not restore the application to a working state
Why: rollback restores the chart manifests to the previous revision but does not undo changes applied by hooks (e.g., a database migration run in pre-upgrade cannot be rolled back automatically).
Detect: rollback completes without error but the application still errors because the schema no longer matches the old code version.
Fix: write migrations to be backward-compatible (expand-contract pattern) so both old and new code can run against the same schema simultaneously.
Connections
cloud-hub · cloud/kubernetes · cloud/argocd · cloud/github-actions
Open Questions
- What monitoring and alerting matter most when this is deployed in production?
- At what scale or workload does this approach hit its practical limits?