Helm Charts

The package manager for Kubernetes - deploy, upgrade, and manage applications with ease

Medium 25 min read Interactive

What is Helm

Helm is the package manager for Kubernetes

Think of it as the "apt" or "yum" for Kubernetes. Helm simplifies the deployment and management of applications by packaging pre-configured Kubernetes resources into reusable charts.

Charts

Packages of pre-configured Kubernetes resources: templates for manifests, default configuration values, documentation, metadata, and dependency management.

Repositories

Storage locations for Helm charts: public charts on Artifact Hub, private repositories, OCI registries, and local chart development.

Releases

Instances of charts running in Kubernetes: unique release names, version tracking, upgrade/rollback capability, and release history.

Helm Workflow

1. Find Chart

Search repositories for existing charts that match your application needs.

2. Configure

Customize values to tailor the chart for your environment and requirements.

3. Install

Deploy the configured chart to your Kubernetes cluster.

4. Manage

Upgrade to new versions, rollback on issues, and track release history.

Helm 3 vs Helm 2

  • No Tiller: Helm 3 removed the server-side component (Tiller)
  • Improved Security: Uses Kubernetes RBAC directly
  • JSON Schema: Chart values validation
  • OCI Support: Store charts in container registries
  • Library Charts: Reusable chart components

Getting Started with Helm

Installation

install-helm.sh
# 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

Basic Commands

repo-management.sh
# Repository Management
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm repo list
helm search repo nginx
chart-install.sh
# Chart Installation
helm install myapp bitnami/nginx
helm install myapp ./mychart
helm install myapp chart.tgz
helm install --dry-run myapp ./chart
release-management.sh
# Release Management
helm list
helm status myapp
helm get values myapp
helm get manifest myapp

# Upgrades & Rollbacks
helm upgrade myapp bitnami/nginx
helm rollback myapp 1
helm history myapp
helm uninstall myapp

Working with Values

values.yaml
# values.yaml
replicaCount: 3

image:
  repository: nginx
  pullPolicy: IfNotPresent
  tag: "1.21.0"

service:
  type: LoadBalancer
  port: 80

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

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 50m
    memory: 64Mi

autoscaling:
  enabled: true
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
install-with-values.sh
# Using values file
helm install myapp bitnami/nginx -f values.yaml

# Using --set flag
helm install myapp bitnami/nginx \
  --set service.type=NodePort \
  --set replicaCount=5

# Multiple values files (later files override)
helm install myapp ./mychart \
  -f values.yaml \
  -f values-prod.yaml \
  --set image.tag=v2.0.0

# From URL
helm install myapp bitnami/nginx \
  -f https://example.com/values.yaml

Values Precedence (Lowest to Highest)

  1. Chart's values.yaml - Default values defined in the chart
  2. Parent chart's values - Values from parent chart override subchart defaults
  3. User-supplied files (-f) - Custom values files provided during install/upgrade
  4. Command line (--set) - Individual values set via command line (highest priority)

Creating Helm Charts

Chart Structure

chart-structure
mychart/                        # Chart directory
  Chart.yaml                    # Chart metadata
  values.yaml                   # Default configuration
  values.schema.json            # JSON schema for values
  charts/                       # Chart dependencies
  templates/                    # Template files
    deployment.yaml
    service.yaml
    ingress.yaml
    hpa.yaml
    NOTES.txt                   # Post-install notes
    _helpers.tpl                # Template helpers
  crds/                         # Custom Resource Definitions
  .helmignore                   # Patterns to ignore
  README.md                     # Chart documentation

Creating Your First Chart

create-chart.sh
# Create new chart
helm create mychart

# Chart structure
tree mychart/

# Validate chart
helm lint mychart/

# Package chart
helm package mychart/

# Install from local directory
helm install myrelease ./mychart
Chart.yaml
apiVersion: v2
name: mychart
description: A Helm chart for my application
type: application  # or 'library' for reusable charts
version: 0.1.0  # Chart version
appVersion: "1.0.0"  # Version of the app being deployed

keywords:
  - webapp
  - nodejs
  - microservice

home: https://github.com/myorg/mychart
sources:
  - https://github.com/myorg/myapp

maintainers:
  - name: John Doe
    email: john@example.com
    url: https://example.com

dependencies:
  - name: postgresql
    version: "11.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled
    tags:
      - database

  - name: redis
    version: "17.x.x"
    repository: https://charts.bitnami.com/bitnami
    alias: cache
    condition: redis.enabled

annotations:
  category: Backend
  licenses: Apache-2.0
chart-values.yaml
# Default values for mychart
replicaCount: 1

image:
  repository: myapp
  pullPolicy: IfNotPresent
  tag: ""  # Defaults to chart appVersion

imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
  create: true
  annotations: {}
  name: ""

podAnnotations: {}
podSecurityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000

securityContext:
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

service:
  type: ClusterIP
  port: 80
  targetPort: http
  annotations: {}

ingress:
  enabled: false
  className: "nginx"
  annotations: {}
  hosts:
    - host: chart-example.local
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []

resources:
  limits:
    cpu: 100m
    memory: 128Mi
  requests:
    cpu: 50m
    memory: 64Mi

autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80
  targetMemoryUtilizationPercentage: 80

# Database configuration
postgresql:
  enabled: true
  auth:
    database: myapp
    username: myuser
    password: changeme

# Cache configuration
redis:
  enabled: false
  auth:
    enabled: true
    password: changeme
values.schema.json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1,
      "maximum": 10,
      "description": "Number of replicas"
    },
    "image": {
      "type": "object",
      "properties": {
        "repository": {
          "type": "string",
          "pattern": "^[a-z0-9-_/]+$"
        },
        "tag": {
          "type": "string",
          "pattern": "^[a-z0-9.-]+$"
        },
        "pullPolicy": {
          "type": "string",
          "enum": ["Always", "IfNotPresent", "Never"]
        }
      },
      "required": ["repository", "pullPolicy"]
    },
    "service": {
      "type": "object",
      "properties": {
        "type": {
          "type": "string",
          "enum": ["ClusterIP", "NodePort", "LoadBalancer"]
        },
        "port": {
          "type": "integer",
          "minimum": 1,
          "maximum": 65535
        }
      }
    }
  },
  "required": ["replicaCount", "image", "service"]
}

Chart Development Best Practices

  • Use semantic versioning for chart versions
  • Keep charts simple and focused on a single application
  • Provide comprehensive values.yaml with sensible defaults
  • Add values.schema.json for validation
  • Include detailed README with examples
  • Use helm lint and helm test for validation
  • Version control your charts

Template Language and Functions

Template Basics

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

Helper Templates

templates/_helpers.tpl
{{/* Expand the name of the chart. */}}
{{- define "mychart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/* Create a default fully qualified app name. */}}
{{- define "mychart.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 }}

{{/* Common labels */}}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
{{ include "mychart.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/* Selector labels */}}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

Template Functions

template-functions.yaml
# String Functions
{{ .Values.name | upper }}          # Convert to uppercase
{{ .Values.name | lower }}          # Convert to lowercase
{{ .Values.name | title }}          # Title case
{{ .Values.name | quote }}          # Add quotes
{{ .Values.text | trunc 63 }}       # Truncate to 63 chars
{{ .Values.text | trimSuffix "-" }} # Remove suffix
{{ .Values.text | replace "old" "new" }} # Replace text

# Default Values
{{ .Values.port | default 8080 }}
{{ .Values.name | default (printf "%s-app" .Release.Name) }}

# Conditionals
{{- if .Values.enabled }}
  # content here
{{- else if .Values.alternate }}
  # alternate content
{{- else }}
  # default content
{{- end }}

# Loops
{{- range .Values.ports }}
- port: {{ . }}
{{- end }}

{{- range $key, $val := .Values.env }}
- name: {{ $key }}
  value: {{ $val }}
{{- end }}

# Type Conversion
{{ int .Values.count }}
{{ float64 .Values.percentage }}
{{ toString .Values.number }}

# Crypto Functions
{{ .Values.password | sha256sum }}
{{ .Values.data | b64enc }}
{{ .Values.encoded | b64dec }}

# YAML/JSON
{{ .Values.config | toYaml | nindent 2 }}
{{ .Values.config | toJson }}
{{ .Values.jsonString | fromJson }}

Template Gotchas

  • Use {{- and -}} to control whitespace
  • Remember that nil is different from empty string
  • Use $ to reference root context in loops
  • Test templates with helm template before installing
  • Be careful with toYaml indentation

Advanced Templating

advanced-templates.yaml
# Using with and range together
{{- with .Values.ingress }}
{{- if .enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "mychart.fullname" $ }}
  annotations:
    {{- range $key, $value := .annotations }}
    {{ $key }}: {{ $value | quote }}
    {{- end }}
spec:
  {{- range .hosts }}
  - host: {{ .host }}
    http:
      paths:
      {{- range .paths }}
      - path: {{ .path }}
        pathType: {{ .pathType }}
        backend:
          service:
            name: {{ include "mychart.fullname" $ }}
            port:
              number: {{ $.Values.service.port }}
      {{- end }}
  {{- end }}
{{- end }}
{{- end }}

# Using lookup function (Helm 3.1+)
{{- $secret := lookup "v1" "Secret" .Release.Namespace "existing-secret" }}
{{- if $secret }}
- name: EXISTING_PASSWORD
  valueFrom:
    secretKeyRef:
      name: existing-secret
      key: password
{{- end }}

# Fail with required
{{ required "A valid .Values.database.host is required!" .Values.database.host }}

Advanced Helm Features

Hooks

templates/hooks/pre-install.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "mychart.fullname" . }}-db-init
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": pre-install,pre-upgrade
    "helm.sh/hook-weight": "1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    metadata:
      name: {{ include "mychart.fullname" . }}-db-init
    spec:
      restartPolicy: Never
      containers:
      - name: db-init
        image: postgres:13
        command:
        - sh
        - -c
        - |
          PGPASSWORD=$POSTGRES_PASSWORD psql -h $DATABASE_HOST \
            -U $DATABASE_USER -d $DATABASE_NAME <<-EOSQL
            CREATE TABLE IF NOT EXISTS users (
              id SERIAL PRIMARY KEY,
              username VARCHAR(50) UNIQUE NOT NULL,
              created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            );
          EOSQL
        env:
        - name: DATABASE_HOST
          value: {{ .Values.database.host }}
        - name: DATABASE_USER
          value: {{ .Values.database.user }}
        - name: POSTGRES_PASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ include "mychart.fullname" . }}-db
              key: password

Tests

templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-test-connection"
  labels:
    {{- include "mychart.labels" . | nindent 4 }}
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: wget
    image: busybox
    command: ['wget']
    args:
    - '{{ include "mychart.fullname" . }}:{{ .Values.service.port }}'
    - '--timeout=5'
    - '--tries=3'
  restartPolicy: Never
---
apiVersion: v1
kind: Pod
metadata:
  name: "{{ include "mychart.fullname" . }}-test-api"
  annotations:
    "helm.sh/hook": test
spec:
  containers:
  - name: test-api
    image: curlimages/curl:latest
    command:
    - sh
    - -c
    - |
      echo "Testing API endpoints..."
      curl -f http://{{ include "mychart.fullname" . }}:{{ .Values.service.port }}/health || exit 1
      curl -f http://{{ include "mychart.fullname" . }}:{{ .Values.service.port }}/ready || exit 1
      echo "All tests passed!"
  restartPolicy: Never

Library Charts

common-library.yaml
# common/Chart.yaml
apiVersion: v2
name: common
description: Common library chart
type: library
version: 1.0.0

# common/templates/_deployment.yaml
{{- define "common.deployment" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "common.fullname" . }}
  labels:
    {{- include "common.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "common.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "common.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
{{- end }}

# Using the library chart in your Chart.yaml:
dependencies:
  - name: common
    version: "1.0.0"
    repository: "file://../common"

# In templates/deployment.yaml:
{{- include "common.deployment" . }}

OCI Registry Support

oci-registry.sh
# Login to OCI registry
helm registry login registry.example.com -u username

# Push chart to OCI registry
helm package mychart/
helm push mychart-0.1.0.tgz oci://registry.example.com/helm-charts

# Pull chart from OCI registry
helm pull oci://registry.example.com/helm-charts/mychart --version 0.1.0

# Install from OCI registry
helm install myrelease oci://registry.example.com/helm-charts/mychart

# Using AWS ECR
aws ecr get-login-password --region us-west-2 | \
  helm registry login --username AWS --password-stdin \
  123456789.dkr.ecr.us-west-2.amazonaws.com

helm push mychart-0.1.0.tgz \
  oci://123456789.dkr.ecr.us-west-2.amazonaws.com/

Post Rendering

post-renderer.sh
#!/bin/bash
# post-renderer.sh
cat <&0 > all.yaml
kustomize build . && rm all.yaml

# kustomization.yaml
resources:
  - all.yaml

patchesStrategicMerge:
  - patches/deployment.yaml

commonLabels:
  environment: production

# Install with post-renderer
chmod +x post-renderer.sh
helm install myrelease ./mychart --post-renderer ./post-renderer.sh

Chart Repositories

Public Repositories: Artifact Hub (hub.helm.sh) is the central registry. Bitnami offers high-quality, production-ready charts.

Private Options: ChartMuseum, Harbor, Nexus Repository, GitLab Package Registry, or GitHub Pages for free hosting of public charts.

Practice Problems

Medium Create a Full-Stack Application Chart

Create a Helm chart for a complete web application with frontend, backend, and database.

Requirements:

  1. Create a parent chart with three subcharts
  2. Frontend: React app served by nginx
  3. Backend: Node.js API with environment-specific configs
  4. Database: PostgreSQL with persistent storage
  5. Configure inter-service communication
  6. Add Ingress for external access

Use Helm dependencies in Chart.yaml to declare subcharts. Use the condition field to allow enabling/disabling each component. Pass database connection details to the backend via environment variables referencing the PostgreSQL subchart.

# fullstack-app/Chart.yaml
apiVersion: v2
name: fullstack-app
description: Full-stack web application
type: application
version: 1.0.0
appVersion: "1.0.0"

dependencies:
  - name: postgresql
    version: "11.9.13"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

# fullstack-app/values.yaml
frontend:
  enabled: true
  replicaCount: 2
  image:
    repository: myapp/frontend
    tag: latest

backend:
  enabled: true
  replicaCount: 3
  image:
    repository: myapp/backend
    tag: latest
  env:
    NODE_ENV: production

postgresql:
  enabled: true
  auth:
    username: appuser
    password: apppass123
    database: myappdb
  persistence:
    enabled: true
    size: 10Gi

ingress:
  enabled: true
  hosts:
    - host: myapp.example.com
      paths:
        - path: /api
          backend: backend
        - path: /
          backend: frontend

# templates/backend-deployment.yaml
{{- if .Values.backend.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "fullstack-app.fullname" . }}-backend
spec:
  replicas: {{ .Values.backend.replicaCount }}
  template:
    spec:
      containers:
      - name: backend
        image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}"
        env:
        {{- if .Values.postgresql.enabled }}
        - name: DATABASE_HOST
          value: {{ include "fullstack-app.fullname" . }}-postgresql
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ include "fullstack-app.fullname" . }}-postgresql
              key: password
        {{- end }}
{{- end }}

Medium Multi-Environment Deployment

Deploy the same application to dev, staging, and production with environment-specific configurations including different resource limits, replica counts, and ingress hosts.

Create separate values files for each environment (values-dev.yaml, values-staging.yaml, values-prod.yaml). Use the -f flag to layer them: helm install -f values.yaml -f values-prod.yaml. Later files override earlier ones.

# values-dev.yaml
environment: development
replicaCount: 1
image:
  tag: develop
resources:
  limits:
    cpu: 200m
    memory: 256Mi
ingress:
  enabled: true
  hosts:
    - host: dev.myapp.example.com

# values-staging.yaml
environment: staging
replicaCount: 2
image:
  tag: staging
resources:
  limits:
    cpu: 500m
    memory: 512Mi

# values-prod.yaml
environment: production
replicaCount: 5
image:
  tag: v1.0.0
resources:
  limits:
    cpu: 1000m
    memory: 1Gi
autoscaling:
  enabled: true
  minReplicas: 5
  maxReplicas: 20
ingress:
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
  tls:
    - secretName: myapp-tls

# Deploy commands
helm install myapp-dev ./mychart -f values.yaml -f values-dev.yaml -n dev
helm install myapp-staging ./mychart -f values.yaml -f values-staging.yaml -n staging
helm install myapp-prod ./mychart -f values.yaml -f values-prod.yaml -n production

Hard GitOps with Helm and ArgoCD

Implement a GitOps workflow using Helm charts and ArgoCD. Structure a Git repository for multiple microservices, create an umbrella chart, implement semantic versioning, set up automated testing with helm test, and configure ArgoCD applications with automatic sync and rollback.

Use a monorepo structure with separate directories for each microservice chart. Create an umbrella chart that declares all microservices as dependencies. ArgoCD Application CRDs can point to the umbrella chart and specific values files for each environment.

# Repository structure
charts/
  microservice-a/
    Chart.yaml
    values.yaml
    templates/
  microservice-b/
    Chart.yaml
    values.yaml
    templates/
  umbrella/
    Chart.yaml    # declares microservice-a and microservice-b as deps
    values.yaml
    values-dev.yaml
    values-prod.yaml

# ArgoCD Application CRD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp-production
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/helm-charts.git
    targetRevision: main
    path: charts/umbrella
    helm:
      valueFiles:
        - values.yaml
        - values-prod.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true