Raw Kubernetes manifests are fine for small examples. They get noisy fast once an application needs environment-specific values, dependencies, upgrades, and rollbacks. Helm gives that pile of YAML a package format: reusable, versioned charts.

What is Helm?

Helm is often called “the package manager for Kubernetes.” It packages Kubernetes resources into charts - reusable, versioned bundles that can be easily shared and deployed.

Without Helm:
┌─────────────────────────────────────────────────┐
│  Manual Deployment                              │
│  kubectl apply -f deployment.yaml               │
│  kubectl apply -f service.yaml                  │
│  kubectl apply -f configmap.yaml                │
│  kubectl apply -f secret.yaml                   │
│  kubectl apply -f ingress.yaml                  │
│  ... (20+ more files)                           │
│  Manual version tracking                        │
│  Manual rollbacks                               │
└─────────────────────────────────────────────────┘

With Helm:
┌─────────────────────────────────────────────────┐
│  Helm Deployment                                │
│  helm install myapp ./mychart                   │
│  - Versioned releases                           │
│  - Easy rollbacks                               │
│  - Templated configurations                     │
│  - Dependency management                        │
└─────────────────────────────────────────────────┘

Helm Benefits

  • Simplified Deployments: Single command deploys entire applications
  • Version Control: Track releases and rollback easily
  • Templating: Parameterize manifests for different environments
  • Dependency Management: Bundle related charts together
  • Sharing: Public repositories for common applications

Helm Architecture

┌─────────────────────────────────────────────────┐
│  Helm CLI                                       │
│  ┌─────────────────────────────────────┐        │
│  │  helm install/upgrade/rollback      │        │
│  └──────────────┬──────────────────────┘        │
│                 │                               │
│                 ▼                               │
│  ┌─────────────────────────────────────┐        │
│  │  Chart + Values                     │        │
│  │  templates/ + values.yaml           │        │
│  └──────────────┬──────────────────────┘        │
│                 │                               │
│                 ▼                               │
│  ┌─────────────────────────────────────┐        │
│  │  Kubernetes API Server              │        │
│  │  (manifests applied)                │        │
│  └─────────────────────────────────────┘        │
└─────────────────────────────────────────────────┘

Installing Helm

# macOS
brew install helm

# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# Windows (Chocolatey)
choco install kubernetes-helm

# Verify installation
helm version

Helm Charts

Chart Structure

mychart/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default configuration values
├── charts/             # Chart dependencies
├── templates/          # Kubernetes manifest templates
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   ├── _helpers.tpl    # Template helpers
│   └── NOTES.txt       # Post-install notes
└── .helmignore         # Files to ignore

Chart.yaml

# Chart.yaml
apiVersion: v2
name: webapp
description: A Helm chart for deploying web applications
type: application
version: 1.0.0
appVersion: "2.0.0"
keywords:
  - webapp
  - nodejs
maintainers:
  - name: Platform Team
    email: [email protected]
dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled

values.yaml

# values.yaml
replicaCount: 3

image:
  repository: myapp
  tag: "v1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80
  targetPort: 8080

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls:
    - secretName: myapp-tls
      hosts:
        - myapp.example.com

resources:
  requests:
    memory: "128Mi"
    cpu: "100m"
  limits:
    memory: "256Mi"
    cpu: "200m"

postgresql:
  enabled: true
  auth:
    database: webapp
    username: webapp

env:
  LOG_LEVEL: info
  CACHE_ENABLED: "true"

Template Examples

deployment.yaml:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "webapp.fullname" . }}
  labels:
    {{- include "webapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "webapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "webapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.targetPort }}
        env:
        {{- range $key, $value := .Values.env }}
        - name: {{ $key }}
          value: {{ $value | quote }}
        {{- end }}
        resources:
          {{- toYaml .Values.resources | nindent 12 }}

service.yaml:

# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ include "webapp.fullname" . }}
  labels:
    {{- include "webapp.labels" . | nindent 4 }}
spec:
  type: {{ .Values.service.type }}
  ports:
  - port: {{ .Values.service.port }}
    targetPort: {{ .Values.service.targetPort }}
    protocol: TCP
  selector:
    {{- include "webapp.selectorLabels" . | nindent 4 }}

_helpers.tpl:

# templates/_helpers.tpl
{{- define "webapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "webapp.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 }}

{{- define "webapp.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "webapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "webapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "webapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Working with Charts

Installing Charts

# Install from local directory
helm install myapp ./mychart

# Install with custom values
helm install myapp ./mychart -f custom-values.yaml

# Install with inline values
helm install myapp ./mychart --set replicaCount=5 --set image.tag=v2.0.0

# Install in specific namespace
helm install myapp ./mychart -n production --create-namespace

# Install from repository
helm install nginx ingress-nginx/ingress-nginx

# Dry run (preview without installing)
helm install myapp ./mychart --dry-run --debug

Upgrading Releases

# Upgrade with new values
helm upgrade myapp ./mychart -f production-values.yaml

# Upgrade with inline values
helm upgrade myapp ./mychart --set image.tag=v2.0.0

# Install or upgrade (idempotent)
helm upgrade --install myapp ./mychart

# Upgrade with atomic rollback on failure
helm upgrade myapp ./mychart --atomic --timeout 5m

Rolling Back

# View release history
helm history myapp

# Rollback to previous version
helm rollback myapp

# Rollback to specific revision
helm rollback myapp 2

Managing Releases

# List releases
helm list
helm list -A  # All namespaces

# Get release status
helm status myapp

# Get release values
helm get values myapp
helm get values myapp --all

# Get rendered manifests
helm get manifest myapp

# Uninstall release
helm uninstall myapp
helm uninstall myapp --keep-history

Helm Repositories

# Add repository
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo add stable https://charts.helm.sh/stable

# Update repositories
helm repo update

# Search repositories
helm search repo nginx
helm search repo bitnami/postgresql --versions

# Search Artifact Hub
helm search hub wordpress

Environment-Specific Values

# values-development.yaml
replicaCount: 1
image:
  tag: "latest"
resources:
  requests:
    memory: "64Mi"
    cpu: "50m"
ingress:
  hosts:
    - host: dev.myapp.example.com

# values-production.yaml
replicaCount: 5
image:
  tag: "v1.0.0"
resources:
  requests:
    memory: "256Mi"
    cpu: "200m"
  limits:
    memory: "512Mi"
    cpu: "500m"
ingress:
  hosts:
    - host: myapp.example.com
# Deploy to different environments
helm install myapp ./mychart -f values-development.yaml -n development
helm install myapp ./mychart -f values-production.yaml -n production

Chart Dependencies

# Chart.yaml
dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled
  - name: redis
    version: "17.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: redis.enabled
# Update dependencies
helm dependency update ./mychart

# Build dependencies
helm dependency build ./mychart

Creating Charts

# Create new chart
helm create mychart

# Package chart
helm package ./mychart

# Lint chart
helm lint ./mychart

# Template locally (debug)
helm template myapp ./mychart --debug

Helm Hooks

Execute actions at specific points in release lifecycle:

# templates/pre-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "webapp.fullname" . }}-db-migrate
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "0"
    "helm.sh/hook-delete-policy": hook-succeeded
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        command: ["./migrate.sh"]
      restartPolicy: Never

Hook Types

HookDescription
pre-installBefore resources installed
post-installAfter resources installed
pre-upgradeBefore upgrade
post-upgradeAfter upgrade
pre-rollbackBefore rollback
post-rollbackAfter rollback
pre-deleteBefore deletion
post-deleteAfter deletion

Troubleshooting

# Debug installation
helm install myapp ./mychart --debug --dry-run

# Check release status
helm status myapp

# View release history
helm history myapp

# Get rendered templates
helm get manifest myapp

# Lint chart
helm lint ./mychart

# Template locally
helm template myapp ./mychart > rendered.yaml

Handy commands

# Repository Management
helm repo add NAME URL
helm repo update
helm repo list
helm search repo KEYWORD

# Release Management
helm install RELEASE CHART
helm upgrade RELEASE CHART
helm rollback RELEASE [REVISION]
helm uninstall RELEASE
helm list
helm status RELEASE
helm history RELEASE

# Chart Development
helm create NAME
helm package CHART
helm lint CHART
helm template RELEASE CHART
helm dependency update CHART

# Values
helm get values RELEASE
helm install RELEASE CHART -f values.yaml
helm install RELEASE CHART --set key=value

What matters in practice

  • Helm simplifies Kubernetes application deployment
  • Charts package related resources together
  • Values enable environment-specific configuration
  • Templates use Go templating for dynamic manifests
  • Releases track deployed chart versions
  • Hooks execute actions at lifecycle points
  • Dependencies manage chart relationships

Where to go next

Helm handles packaging and release lifecycle. The next layer is Operators, where custom controllers manage application behavior inside the cluster.

Further reading