helm-expert
Expert Helm knowledge covering chart structure, template functions, named templates with _helpers.tpl, lifecycle hooks, umbrella charts and subcharts, values hierarchy, Helmfile for multi-chart releases, helm-diff for dry-run review, secrets manageme
Installation
npx clawhub@latest install helm-expertView the full skill documentation and source below.
Documentation
Helm Expert
Helm is Kubernetes' package manager, but using it well requires understanding Go templates, value
precedence, and release lifecycle management. A poorly structured Helm chart becomes a maintenance
nightmare of untestable if/else spaghetti. A well-structured chart reads like documentation and composes
cleanly across environments.
Core Mental Model
A Helm chart is a parameterized Kubernetes manifest generator. The values.yaml is the API contract
of your chart — everything a user needs to configure should be there, with sensible defaults. Templates
should be thin: logic lives in _helpers.tpl as named templates, and manifests are assembled from
those helpers. The release cycle (install → upgrade → rollback → uninstall) drives the lifecycle hooks.
Helmfile coordinates multiple charts the way docker-compose coordinates containers.
Chart Structure
my-service/
├── Chart.yaml # Chart metadata (name, version, appVersion, dependencies)
├── values.yaml # Default values (the user's API)
├── charts/ # Subchart dependencies (downloaded by helm dep update)
├── crds/ # Custom Resource Definitions (installed before templates)
├── templates/
│ ├── _helpers.tpl # Named templates (no YAML output)
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── serviceaccount.yaml
│ ├── hpa.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ ├── NOTES.txt # Post-install instructions
│ └── tests/
│ └── test-connection.yaml
└── .helmignore
Chart.yaml
apiVersion: v2
name: order-api
description: Order processing microservice
type: application # or: library (no deployable resources, only named templates)
version: 1.2.3 # Chart version (semver) — bump on chart changes
appVersion: "2.5.0" # App version — informational only, often the Docker tag
# Dependencies (helm dep update downloads these to charts/)
dependencies:
- name: postgresql
version: "12.x.x"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
alias: db # Override release name within chart
- name: redis
version: "17.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
_helpers.tpl: Named Templates
{{/* /templates/_helpers.tpl */}}
{{/*
Expand the name of the chart.
We truncate at 63 chars because some Kubernetes name fields are limited to this.
*/}}
{{- define "order-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "order-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Standard labels — include on every resource for consistent selection and GitOps
*/}}
{{- define "order-api.labels" -}}
helm.sh/chart: {{ include "order-api.chart" . }}
{{ include "order-api.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels — used in Service selectors, Deployment matchLabels
Must be stable (cannot add/remove without recreating resources)
*/}}
{{- define "order-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "order-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Service account name
*/}}
{{- define "order-api.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "order-api.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Image name with tag
*/}}
{{- define "order-api.image" -}}
{{- $registry := .Values.image.registry | default "ghcr.io" -}}
{{- $tag := .Values.image.tag | default .Chart.AppVersion -}}
{{- printf "%s/%s:%s" $registry .Values.image.repository $tag }}
{{- end }}
Deployment Template (Production-Ready)
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "order-api.fullname" . }}
labels:
{{- include "order-api.labels" . | nindent 4 }}
annotations:
{{- with .Values.deployment.annotations }}
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "order-api.selectorLabels" . | nindent 6 }}
strategy:
{{- toYaml .Values.deployment.strategy | nindent 4 }}
template:
metadata:
annotations:
# Force pod restart when config changes
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "order-api.selectorLabels" . | nindent 8 }}
{{- with .Values.podLabels }}
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "order-api.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- if .Values.initContainers }}
initContainers:
{{- toYaml .Values.initContainers | nindent 8 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
image: {{ include "order-api.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
{{- if .Values.metrics.enabled }}
- name: metrics
containerPort: {{ .Values.metrics.port }}
protocol: TCP
{{- end }}
{{- if .Values.env }}
env:
{{- range .Values.env }}
- name: {{ .name }}
{{- if .value }}
value: {{ .value | quote }}
{{- else if .valueFrom }}
valueFrom:
{{- toYaml .valueFrom | nindent 16 }}
{{- end }}
{{- end }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "order-api.fullname" . }}
{{- if .Values.existingSecret }}
- secretRef:
name: {{ .Values.existingSecret }}
{{- end }}
livenessProbe:
{{- toYaml .Values.livenessProbe | nindent 12 }}
readinessProbe:
{{- toYaml .Values.readinessProbe | nindent 12 }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.volumeMounts }}
volumeMounts:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .Values.volumes }}
volumes:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
Helm Hooks
# Pre-install DB migration job
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "order-api.fullname" . }}-migration
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5" # Lower weight runs first
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
spec:
restartPolicy: Never
containers:
- name: migration
image: {{ include "order-api.image" . }}
command: ["./migrate", "up"]
envFrom:
- secretRef:
name: {{ include "order-api.fullname" . }}-db
---
# Post-install smoke test
apiVersion: v1
kind: Pod
metadata:
name: {{ include "order-api.fullname" . }}-test
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl:latest
command:
- /bin/sh
- -c
- |
curl -f http://{{ include "order-api.fullname" . }}/health || exit 1
echo "Health check passed"
Helmfile for Multi-Environment Releases
# helmfile.yaml
bases:
- environments.yaml # Environment-specific overrides
repositories:
- name: bitnami
url: https://charts.bitnami.com/bitnami
- name: internal
url: oci://us-central1-docker.pkg.dev/my-project/helm-charts
environments:
development:
values:
- envs/development.yaml
secrets:
- envs/development.secrets.yaml.enc # SOPS-encrypted
production:
values:
- envs/production.yaml
secrets:
- envs/production.secrets.yaml.enc
releases:
- name: order-api
namespace: order-service
chart: internal/order-api
version: "~1.2.0" # Allows patch updates
createNamespace: true
wait: true # Wait for rollout to complete
timeout: 300 # 5 minute timeout
atomic: true # Roll back automatically on failure
values:
- values/order-api.yaml
- values/order-api.{{ .Environment.Name }}.yaml
set:
- name: image.tag
value: {{ requiredEnv "IMAGE_TAG" }}
- name: postgresql
namespace: order-service
chart: bitnami/postgresql
version: "12.1.0" # Pin exact version for databases!
condition: postgresql.enabled
values:
- values/postgresql.yaml
hooks:
- events: ["presync"]
command: "kubectl"
args: ["create", "namespace", "order-service", "--dry-run=client", "-o", "yaml"]
# Helmfile commands
helmfile diff # Preview changes (requires helm-diff plugin)
helmfile apply # Apply changes (diff + sync)
helmfile sync # Sync all releases
helmfile sync --selector name=order-api # Single release
helmfile destroy # Uninstall all
Helm Secrets with SOPS
# Install plugin
helm plugin install https://github.com/jkroepke/helm-secrets
# Encrypt secrets file with SOPS (using AWS KMS or GCP KMS)
cat secrets.yaml
# db_password: my-secret-password
sops --encrypt \
--gcp-kms projects/my-project/locations/global/keyRings/helm/cryptoKeys/secrets \
secrets.yaml > secrets.yaml.enc
# Use in helm command
helm upgrade --install order-api . \
-f values.yaml \
-f secrets://secrets.yaml.enc
# Or with helmfile (automatic SOPS decryption)
secrets:
- path/to/secrets.yaml.enc
OCI Registry
# Push chart to OCI registry (Helm 3.8+)
helm package ./order-api
helm push order-api-1.2.3.tgz oci://us-central1-docker.pkg.dev/my-project/helm-charts
# Pull and install from OCI
helm install order-api \
oci://us-central1-docker.pkg.dev/my-project/helm-charts/order-api \
--version 1.2.3
# Login to OCI registry
gcloud auth print-access-token | \
helm registry login -u oauth2accesstoken --password-stdin \
us-central1-docker.pkg.dev
Anti-Patterns
❌ image.tag: latest in values.yaml — always pin to a specific tag in production
❌ Logic in deployment.yaml instead of _helpers.tpl — named templates are testable
❌ Not setting checksum/config annotation — config changes won't trigger pod restarts
❌ helm upgrade without --atomic — failed upgrades leave releases in broken state
❌ Storing secrets in values.yaml in plaintext — use SOPS, Vault, or external secrets
❌ Not using required for mandatory values — missing values fail silently with wrong defaults
❌ Hardcoded namespace in templates — use {{ .Release.Namespace }} for portability
❌ Missing strategy in Deployment — default RollingUpdate settings are too conservative
Quick Reference
Template functions:
{{ required "msg" .Values.key }} → fail if value not set
{{ default "fallback" .Values.key }} → use fallback if not set
{{ .Values.key | quote }} → wrap in quotes
{{ .Values.key | upper }} → uppercase
{{ toYaml .Values.key | nindent 4 }} → render YAML with indent
{{ include "chart.name" . }} → call named template
{{ tpl .Values.template . }} → render string as template
Helm commands:
helm install NAME CHART -f values.yaml --dry-run → Preview
helm upgrade --install NAME CHART -f values.yaml --atomic
helm history NAME → Release history
helm rollback NAME [revision] → Roll back
helm get values NAME → Deployed values
helm get manifest NAME → Deployed manifests
helm diff upgrade NAME CHART -f values.yaml → Preview diff (plugin)
helm lint . → Validate chart
helm template . -f values.yaml → Render locally