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
# 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
# Repository Management
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm repo list
helm search repo nginx
# Chart Installation
helm install myapp bitnami/nginx
helm install myapp ./mychart
helm install myapp chart.tgz
helm install --dry-run myapp ./chart
# 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
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
# 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)
- Chart's values.yaml - Default values defined in the chart
- Parent chart's values - Values from parent chart override subchart defaults
- User-supplied files (-f) - Custom values files provided during install/upgrade
- Command line (--set) - Individual values set via command line (highest priority)
Creating Helm Charts
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 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
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
# 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
{
"$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
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
{{/* 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
# 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
nilis different from empty string - Use
$to reference root context in loops - Test templates with
helm templatebefore installing - Be careful with
toYamlindentation
Advanced Templating
# 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
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
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/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
# 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
#!/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:
- Create a parent chart with three subcharts
- Frontend: React app served by nginx
- Backend: Node.js API with environment-specific configs
- Database: PostgreSQL with persistent storage
- Configure inter-service communication
- 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