Kubernetes Helm: Package Manager for Kubernetes

If you've managed Kubernetes applications in production, you've experienced the pain of YAML proliferation. A single microservice might require a Deployment, Service, ConfigMap, Secret, Ingress,...

Key Insights

  • Helm eliminates YAML sprawl by packaging Kubernetes manifests into reusable, versioned charts with templating support that adapts to different environments
  • The three-tier architecture of charts (packages), releases (deployments), and repositories (distribution) mirrors traditional package managers like apt or npm
  • Production Helm usage requires discipline around values management, dependency locking, and treating charts as versioned artifacts with proper testing pipelines

Introduction to Helm

If you’ve managed Kubernetes applications in production, you’ve experienced the pain of YAML proliferation. A single microservice might require a Deployment, Service, ConfigMap, Secret, Ingress, ServiceAccount, and various RBAC resources. Multiply that by dozens of services across multiple environments, and you’re drowning in nearly-identical manifests with subtle differences.

Helm solves this by introducing templating and packaging to Kubernetes. Think of it as apt, yum, or npm for Kubernetes—a package manager that bundles related resources together, parameterizes configuration, and provides versioning and rollback capabilities. Instead of maintaining separate YAML files for dev, staging, and production, you maintain one templated chart and override values per environment.

The alternative is managing raw manifests with tools like Kustomize or custom scripts. While Kustomize has its place for simple overlays, Helm’s templating engine and ecosystem of pre-built charts make it the de facto standard for complex deployments.

Core Helm Concepts

Helm operates on three fundamental concepts that work together to manage Kubernetes applications.

Charts are the packaging format. A chart is a directory structure containing templated Kubernetes manifests, metadata, and default values. Charts can be versioned, shared, and distributed through repositories.

Releases are running instances of charts. When you install a chart, Helm creates a release with a unique name. You can have multiple releases of the same chart in a cluster, each with different configurations.

Repositories are collections of packaged charts. Public repositories like Artifact Hub host thousands of community charts, while private repositories let you distribute internal applications.

A basic chart directory looks like this:

mychart/
├── Chart.yaml          # Metadata about the chart
├── values.yaml         # Default configuration values
├── charts/             # Dependent charts
└── templates/          # Kubernetes manifest templates
    ├── deployment.yaml
    ├── service.yaml
    ├── _helpers.tpl    # Template helpers
    └── NOTES.txt       # Post-installation notes

The templates/ directory contains Go template files that generate Kubernetes manifests. The values.yaml file provides default configuration, which users override during installation.

Installing and Using Helm Charts

Install Helm from the official releases or use a package manager:

# macOS
brew install helm

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

Add a chart repository and search for available charts:

helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo postgresql

Install PostgreSQL with custom values:

helm install my-postgres bitnami/postgresql \
  --set auth.postgresPassword=secretpassword \
  --set primary.persistence.size=20Gi \
  --namespace database \
  --create-namespace

The --set flag overrides individual values. For complex configurations, use a values file:

helm install my-postgres bitnami/postgresql \
  -f custom-values.yaml \
  --namespace database

Manage releases with these commands:

# List all releases
helm list --all-namespaces

# Check release status
helm status my-postgres -n database

# Upgrade with new values
helm upgrade my-postgres bitnami/postgresql \
  --set primary.persistence.size=50Gi \
  -n database

# View release history
helm history my-postgres -n database

# Uninstall
helm uninstall my-postgres -n database

Creating Custom Helm Charts

Generate a chart scaffold:

helm create myapp

This creates a complete chart structure with example templates. Start by editing Chart.yaml:

apiVersion: v2
name: myapp
description: A Helm chart for my application
type: application
version: 0.1.0
appVersion: "1.0.0"

The version field tracks chart changes, while appVersion tracks the application version.

Define default values in values.yaml:

replicaCount: 2

image:
  repository: mycompany/myapp
  pullPolicy: IfNotPresent
  tag: "1.0.0"

service:
  type: ClusterIP
  port: 8080

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

ingress:
  enabled: false
  className: nginx
  host: myapp.example.com

Create a templated deployment in templates/deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.port }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

The double curly braces {{ }} denote template directives. .Values accesses the values hierarchy, .Chart accesses chart metadata, and functions like toYaml format data appropriately.

Helm Templating and Values

Helm’s templating power comes from Go templates plus Sprig functions. Use conditionals to enable optional features:

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "myapp.fullname" . }}
spec:
  ingressClassName: {{ .Values.ingress.className }}
  rules:
  - host: {{ .Values.ingress.host }}
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: {{ include "myapp.fullname" . }}
            port:
              number: {{ .Values.service.port }}
{{- end }}

Create environment-specific value files:

values-dev.yaml:

replicaCount: 1
image:
  tag: "latest"
ingress:
  enabled: true
  host: myapp-dev.internal

values-prod.yaml:

replicaCount: 3
image:
  tag: "1.0.0"
ingress:
  enabled: true
  host: myapp.example.com
resources:
  limits:
    cpu: 1000m
    memory: 1Gi

Install with environment-specific values:

helm install myapp-prod ./myapp -f values-prod.yaml -n production

Debug templates without installing:

helm template myapp ./myapp -f values-dev.yaml

This renders templates locally, perfect for validating syntax before deployment.

Helm Chart Dependencies and Lifecycle

Charts can depend on other charts. Define dependencies in 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 ./myapp

This downloads dependent charts into the charts/ directory. Override dependent chart values in your values.yaml:

postgresql:
  enabled: true
  auth:
    database: myapp
    username: myapp

redis:
  enabled: true
  architecture: standalone

Helm tracks release history, enabling rollbacks:

# Upgrade gone wrong
helm upgrade myapp-prod ./myapp -f values-prod.yaml

# Rollback to previous revision
helm rollback myapp-prod

# Rollback to specific revision
helm rollback myapp-prod 3

Each upgrade creates a new revision. Helm stores release data in Kubernetes Secrets by default, maintaining complete history.

Best Practices and Production Considerations

Lint and test charts before deployment:

helm lint ./myapp

Define tests in templates/tests/:

apiVersion: v1
kind: Pod
metadata:
  name: {{ include "myapp.fullname" . }}-test
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: curl
    image: curlimages/curl:latest
    command: ['curl']
    args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health']
  restartPolicy: Never

Run tests after installation:

helm test myapp-prod -n production

Version charts semantically. Increment the patch version for bug fixes, minor for backward-compatible changes, major for breaking changes. Lock chart versions in production:

helm install myapp-prod ./myapp --version 1.2.3

Never store secrets in values files. Use external secret management:

# Reference external secrets
env:
  - name: DB_PASSWORD
    valueFrom:
      secretKeyRef:
        name: myapp-secrets
        key: db-password

Or use tools like sealed-secrets or external-secrets-operator to manage sensitive data.

Use .helmignore to exclude unnecessary files from packaged charts:

.git/
.gitignore
*.md
.DS_Store

Pin dependency versions in production. Avoid ranges like "^1.0.0" that could introduce unexpected changes.

Helm transforms Kubernetes complexity into manageable, versioned packages. While it adds another layer to learn, the payoff in reduced YAML duplication and improved deployment consistency makes it essential for any serious Kubernetes operation. Start with public charts to understand patterns, then build custom charts as your applications mature.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.