Skip to main content
DevOps & CloudDocumented

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

Share:

Installation

npx clawhub@latest install helm-expert

View 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