Overview

Helm is the package manager for Kubernetes. A chart is a collection of YAML templates plus a values.yaml that parameterises them. A release is a deployed instance of a chart in a cluster. Helm tracks release history, enabling rollbacks without touching Git.

Chart (on disk / OCI registry)
  ├── Chart.yaml          — name, version, appVersion, dependencies
  ├── values.yaml         — default parameter values
  ├── templates/          — Go-templated Kubernetes manifests
  │   ├── deployment.yaml
  │   ├── service.yaml
  │   ├── ingress.yaml
  │   ├── hpa.yaml
  │   └── _helpers.tpl    — named template partials
  ├── charts/             — vendored subcharts
  └── tests/              — helm test pods

Release (in cluster)
  └── stored as Secrets in the release namespace (base64 + gzip)

Chart Structure and Authoring

Chart.yaml

apiVersion: v2
name: payments-api
description: Payments API service
type: application          # or "library" for shared helpers
version: 0.5.2             # chart version (semver)
appVersion: "1.14.0"       # application version (informational)
kubeVersion: ">=1.28.0"
dependencies:
- name: postgresql
  version: "15.5.x"
  repository: https://charts.bitnami.com/bitnami
  condition: postgresql.enabled   # only install if enabled in values

values.yaml — Well-Structured Defaults

# values.yaml
replicaCount: 2

image:
  repository: ghcr.io/acme/payments-api
  pullPolicy: IfNotPresent
  tag: ""                  # overridden by CI with the git SHA

serviceAccount:
  create: true
  annotations: {}          # add IRSA annotation here
  name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 2000

securityContext:
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: [ALL]

service:
  type: ClusterIP
  port: 8080

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts: []
  tls: []

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 256Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 60

podDisruptionBudget:
  enabled: true
  minAvailable: 1

nodeSelector: {}
tolerations: []
affinity: {}

topologySpreadConstraints:
- maxSkew: 1
  topologyKey: topology.kubernetes.io/zone
  whenUnsatisfiable: DoNotSchedule
  labelSelector:
    matchLabels:
      app.kubernetes.io/name: payments-api

env: []
envFrom: []

livenessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /readyz
    port: 8080
  initialDelaySeconds: 3
  periodSeconds: 5

postgresql:
  enabled: false           # use external DB by default

templates/deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "payments-api.fullname" . }}
  labels:
    {{- include "payments-api.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "payments-api.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      annotations:
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
        {{- with .Values.podAnnotations }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
      labels:
        {{- include "payments-api.labels" . | nindent 8 }}
        {{- with .Values.podLabels }}
        {{- toYaml . | nindent 8 }}
        {{- end }}
    spec:
      serviceAccountName: {{ include "payments-api.serviceAccountName" . }}
      securityContext:
        {{- toYaml .Values.podSecurityContext | nindent 8 }}
      containers:
      - name: {{ .Chart.Name }}
        securityContext:
          {{- toYaml .Values.securityContext | nindent 10 }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - name: http
          containerPort: {{ .Values.service.port }}
          protocol: TCP
        livenessProbe:
          {{- toYaml .Values.livenessProbe | nindent 10 }}
        readinessProbe:
          {{- toYaml .Values.readinessProbe | nindent 10 }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
        {{- with .Values.env }}
        env:
          {{- toYaml . | nindent 10 }}
        {{- end }}
      {{- with .Values.topologySpreadConstraints }}
      topologySpreadConstraints:
        {{- toYaml . | nindent 8 }}
      {{- end }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}

templates/_helpers.tpl

{{/*
Expand the name of the chart.
*/}}
{{- define "payments-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "payments-api.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{/*
Common labels — applied to all resources.
*/}}
{{- define "payments-api.labels" -}}
helm.sh/chart: {{ include "payments-api.chart" . }}
{{ include "payments-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 matchLabels and service selector.
*/}}
{{- define "payments-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "payments-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Library Charts — Shared Helpers

Library charts contain only _helpers.tpl — no deployable templates. Multiple application charts import the library to share common label/annotation patterns.

# platform-lib/Chart.yaml
apiVersion: v2
name: platform-lib
type: library
version: 1.2.0
# In an app chart's Chart.yaml
dependencies:
- name: platform-lib
  version: "1.2.x"
  repository: oci://ghcr.io/acme/charts
# In app chart templates — use library helper
metadata:
  labels:
    {{- include "platform-lib.labels" . | nindent 4 }}
  annotations:
    {{- include "platform-lib.standardAnnotations" . | nindent 4 }}

Release Management

Install and Upgrade

# Add a chart repository
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Install a chart
helm install payments-api ./charts/payments-api \
  --namespace production \
  --create-namespace \
  --values values-production.yaml \
  --set image.tag=sha-abc123 \
  --atomic \              # rollback automatically if upgrade fails
  --timeout 5m \
  --wait                  # wait until all pods are ready

# Upgrade a release
helm upgrade payments-api ./charts/payments-api \
  --namespace production \
  --values values-production.yaml \
  --set image.tag=sha-newsha \
  --atomic \
  --timeout 5m \
  --cleanup-on-fail

# Install or upgrade (idempotent — safe for CI/CD)
helm upgrade --install payments-api ./charts/payments-api \
  --namespace production \
  --create-namespace \
  --values values-production.yaml \
  --set image.tag=sha-abc123 \
  --atomic

Inspecting Releases

# List all releases
helm list -A

# Get release status
helm status payments-api -n production

# Show what values are being used (user-supplied + defaults merged)
helm get values payments-api -n production
helm get values payments-api -n production --all   # includes chart defaults

# Show rendered manifests for a release
helm get manifest payments-api -n production

# Show release history
helm history payments-api -n production

Rollback

# Rollback to previous revision
helm rollback payments-api -n production

# Rollback to specific revision
helm rollback payments-api 3 -n production

# Rollback is immediate — Helm applies the previous rendered manifests
# Argo CD will notice the drift and may re-sync; disable auto-sync during incident

OCI Registry — Helm Charts as OCI Artifacts

Modern approach: store charts in an OCI container registry alongside images. No helm repo add needed.

# Push chart to OCI registry
helm package ./charts/payments-api
helm push payments-api-0.5.2.tgz oci://ghcr.io/acme/charts

# Pull and install from OCI
helm install payments-api \
  oci://ghcr.io/acme/charts/payments-api \
  --version 0.5.2 \
  --namespace production

# Login to OCI registry
helm registry login ghcr.io \
  --username $GITHUB_ACTOR \
  --password $GITHUB_TOKEN

Helm Secrets — Encrypted Values

For secrets that must live alongside chart values, use helm-secrets (wraps SOPS):

# Install helm-secrets plugin
helm plugin install https://github.com/jkroepke/helm-secrets

# Encrypt a values file with SOPS (AWS KMS)
sops --encrypt \
  --kms arn:aws:kms:us-east-1:123456789012:key/mrk-... \
  values-production-secrets.yaml > values-production-secrets.enc.yaml

# Deploy with encrypted values (helm-secrets decrypts at runtime)
helm secrets upgrade payments-api ./charts/payments-api \
  -f values-production.yaml \
  -f values-production-secrets.enc.yaml \
  --namespace production

Helm Test

# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "payments-api.fullname" . }}-test-connection"
  labels:
    {{- include "payments-api.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  restartPolicy: Never
  containers:
  - name: curl
    image: curlimages/curl:latest
    command: ['curl', '--fail', 'http://{{ include "payments-api.fullname" . }}:{{ .Values.service.port }}/healthz']
# Run tests after install/upgrade
helm test payments-api -n production --logs

Helm in Argo CD

# Argo CD Application using Helm
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: payments-api
  namespace: argocd
spec:
  project: production
  source:
    repoURL: oci://ghcr.io/acme/charts
    chart: payments-api
    targetRevision: 0.5.2
    helm:
      releaseName: payments-api
      valueFiles:
      - values-production.yaml
      parameters:
      - name: image.tag
        value: sha-abc123
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
    - CreateNamespace=true
    - ServerSideApply=true

Common Pitfalls

ProblemCauseFix
Release stuck in pending-upgradePrevious upgrade crashed mid-wayhelm rollback payments-api -n production or delete the stuck secret
Values not applyingWrong --values file path or mistyped --set keyhelm get values to verify; helm template to dry-run render
chart requires kubeVersionCluster version too oldUpgrade cluster or remove version constraint if non-critical
Subchart values not passedMissing subchart prefix in valuesUse postgresql.enabled: true not enabled: true for subcharts
UPGRADE FAILED: rendered manifests contain a resource that already existsResource created outside Helmhelm upgrade --force or kubectl annotate/label resource with Helm ownership
Argo CD re-syncs despite no changeHelm renders non-deterministic output (random labels, timestamps)Use helm.sh/chart label carefully; avoid randAlphaNum in templates