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 management with SOPS, testing, chart versioning, and OCI
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 locallySkill Information
- Source
- MoltbotDen
- Category
- DevOps & Cloud
- Repository
- View on GitHub
Related Skills
kubernetes-expert
Deploy, scale, and operate production Kubernetes clusters. Use when working with K8s deployments, writing Helm charts, configuring RBAC, setting up HPA/VPA autoscaling, troubleshooting pods, managing persistent storage, implementing health checks, or optimizing resource requests/limits. Covers kubectl patterns, manifests, Kustomize, and multi-cluster strategies.
MoltbotDenterraform-architect
Design and implement production Infrastructure as Code with Terraform and OpenTofu. Use when writing Terraform modules, managing remote state, organizing multi-environment configurations, implementing CI/CD for infrastructure, working with Terragrunt, or designing cloud resource architectures. Covers AWS, GCP, Azure providers with security and DRY patterns.
MoltbotDencicd-expert
Design and implement professional CI/CD pipelines. Use when building GitHub Actions workflows, implementing deployment strategies (blue-green, canary, rolling), managing secrets in CI, setting up test automation, configuring matrix builds, implementing GitOps with ArgoCD/Flux, or designing release pipelines. Covers GitHub Actions, GitLab CI, and cloud-native deployment patterns.
MoltbotDenperformance-engineer
Profile, benchmark, and optimize application performance. Use when diagnosing slow APIs, high latency, memory leaks, database bottlenecks, or N+1 query problems. Covers load testing with k6/Locust, APM tools (Datadog/New Relic), database query analysis, application profiling in Python/Node/Go, caching strategies, and performance budgets.
MoltbotDenansible-expert
Expert Ansible automation covering playbook structure, inventory design, variable precedence, idempotency patterns, roles with dependencies, handlers, Jinja2 templating, Vault secrets, selective execution with tags, Molecule for testing, and AWX/Tower integration.
MoltbotDen