Helm Charts
Helm turns Kubernetes manifests into reusable packages that can be managed per environment. It is valuable when the same application must be installed consistently across environments; it is unnecessary abstraction when it only hides a single YAML file.
Quick Decision
| Need | Use Helm | Watch Out |
|---|---|---|
| Dev/stage/prod values differ | Yes | Values files should not drift |
| Same chart installed for many tenants | Yes | Template complexity must stay limited |
| One service, one environment, little YAML | Not necessary | Plain manifests can be more readable |
| Rollback and release history required | Yes | Hook and migration behavior must be tested |
Production Checklist
- Problem: Which repetition or environment difference does Helm simplify?
- Solution: Are chart version, app version, values schema, secret approach, and rollback path clear?
- Trade-off: Packaging and reuse improve; template debugging and value inheritance get more complex.
- Failure mode: Wrong values, failed hooks, partial releases, migration rollback, and immutable field errors need plans.
- Measurement: Track release success rate, rollback count, deployment duration, template diff, and chart drift.
- Security/cost: Secrets should not be embedded in charts; over-parameterized charts increase maintenance cost.
What is Helm?
Helm is the package manager for Kubernetes. It helps you manage Kubernetes applications through Helm Charts, which are collections of files that describe a related set of Kubernetes resources.
Core Concepts
Chart
A Chart is a Helm package containing all of the resource definitions necessary to run an application, tool, or service inside a Kubernetes cluster.
Release
A Release is an instance of a chart running in a Kubernetes cluster. A single chart can be installed multiple times into the same cluster with different release names.
Repository
A Repository is a place where charts can be collected and shared.
Basic Chart Structure
mychart/
Chart.yaml # Chart metadata
values.yaml # Default configuration values
charts/ # Chart dependencies
templates/ # Template files
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml
_helpers.tpl # Template helpers
.helmignore # Files to ignore when packagingChart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for my application
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- web
- application
home: https://github.com/mycompany/myapp
sources:
- https://github.com/mycompany/myapp
maintainers:
- name: DevOps Team
email: devops@mycompany.com
dependencies:
- name: postgresql
version: 11.6.12
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
- name: redis
version: 16.9.11
repository: https://charts.bitnami.com/bitnami
condition: redis.enabledValues.yaml
# Default values for myapp
replicaCount: 3
image:
repository: myapp
pullPolicy: IfNotPresent
tag: "1.0.0"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations: {}
podSecurityContext:
fsGroup: 2000
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: chart-example.local
paths:
- path: /
pathType: ImplementationSpecific
tls: []
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 100
targetCPUUtilizationPercentage: 80
nodeSelector: {}
tolerations: []
affinity: {}
# Application-specific configuration
config:
database:
host: ""
port: 5432
name: myapp
user: myapp
redis:
host: ""
port: 6379
logging:
level: INFO
features:
newFeature: false
# External dependencies
postgresql:
enabled: true
auth:
postgresPassword: secretPassword
username: myapp
password: myappPassword
database: myapp
redis:
enabled: true
auth:
enabled: falseTemplate Files
Deployment Template
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
{{- with .Values.podAnnotations }}
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "myapp.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
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
env:
- name: DATABASE_HOST
value: {{ include "myapp.databaseHost" . }}
- name: DATABASE_PORT
value: "{{ .Values.config.database.port }}"
- name: DATABASE_NAME
value: {{ .Values.config.database.name }}
- name: DATABASE_USER
value: {{ .Values.config.database.user }}
- name: DATABASE_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "myapp.fullname" . }}-db
key: password
- name: REDIS_HOST
value: {{ include "myapp.redisHost" . }}
- name: REDIS_PORT
value: "{{ .Values.config.redis.port }}"
- name: LOG_LEVEL
value: {{ .Values.config.logging.level }}
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
resources:
{{- toYaml .Values.resources | nindent 12 }}
volumeMounts:
- name: config-volume
mountPath: /etc/config
readOnly: true
volumes:
- name: config-volume
configMap:
name: {{ include "myapp.fullname" . }}-config
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}Service Template
# templates/service.yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
{{- include "myapp.selectorLabels" . | nindent 4 }}Ingress Template
# templates/ingress.yaml
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ $.Values.service.port }}
{{- end }}
{{- end }}
{{- end }}ConfigMap Template
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
labels:
{{- include "myapp.labels" . | nindent 4 }}
data:
APP_NAME: {{ include "myapp.fullname" . }}
ENVIRONMENT: {{ .Values.global.environment | default "production" }}
NEW_FEATURE_ENABLED: {{ .Values.config.features.newFeature | quote }}
config.yaml: |
server:
port: {{ .Values.service.targetPort }}
database:
host: {{ include "myapp.databaseHost" . }}
port: {{ .Values.config.database.port }}
name: {{ .Values.config.database.name }}
redis:
host: {{ include "myapp.redisHost" . }}
port: {{ .Values.config.redis.port }}
logging:
level: {{ .Values.config.logging.level }}Secret Template
# templates/secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: {{ include "myapp.fullname" . }}-db
labels:
{{- include "myapp.labels" . | nindent 4 }}
type: Opaque
data:
{{- if .Values.postgresql.enabled }}
password: {{ .Values.postgresql.auth.password | b64enc }}
{{- else }}
password: {{ required "Database password is required when postgresql is disabled" .Values.config.database.password | b64enc }}
{{- end }}Helper Templates
# templates/_helpers.tpl
{{/*
Expand the name of the chart.
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "myapp.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 }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
{{/*
Database host helper
*/}}
{{- define "myapp.databaseHost" -}}
{{- if .Values.postgresql.enabled }}
{{- printf "%s-postgresql" (include "myapp.fullname" .) }}
{{- else }}
{{- required "Database host is required when postgresql is disabled" .Values.config.database.host }}
{{- end }}
{{- end }}
{{/*
Redis host helper
*/}}
{{- define "myapp.redisHost" -}}
{{- if .Values.redis.enabled }}
{{- printf "%s-redis-master" (include "myapp.fullname" .) }}
{{- else }}
{{- required "Redis host is required when redis is disabled" .Values.config.redis.host }}
{{- end }}
{{- end }}HPA Template
# templates/hpa.yaml
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "myapp.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}Multi-Environment Values
values-dev.yaml
# Development environment values
replicaCount: 1
image:
repository: myapp
tag: "dev"
pullPolicy: Always
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
config:
logging:
level: DEBUG
features:
newFeature: true
postgresql:
enabled: true
auth:
postgresPassword: devPassword
password: devPassword
redis:
enabled: true
ingress:
enabled: true
className: nginx
hosts:
- host: myapp-dev.internal
paths:
- path: /
pathType: Prefixvalues-staging.yaml
# Staging environment values
replicaCount: 2
image:
repository: myapp
tag: "staging"
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 150m
memory: 192Mi
config:
logging:
level: INFO
features:
newFeature: true
postgresql:
enabled: true
auth:
postgresPassword: stagingPassword
password: stagingPassword
redis:
enabled: true
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-staging
hosts:
- host: myapp-staging.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-staging-tls
hosts:
- myapp-staging.example.comvalues-prod.yaml
# Production environment values
replicaCount: 5
image:
repository: myapp
tag: "1.0.0"
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 250m
memory: 256Mi
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 20
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
config:
logging:
level: WARN
features:
newFeature: false
postgresql:
enabled: false
# Use external database in production
config:
database:
host: prod-db.example.com
password: productionPassword
redis:
enabled: false
config:
redis:
host: prod-redis.example.com
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: myapp-prod-tls
hosts:
- myapp.example.com
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchExpressions:
- key: app.kubernetes.io/name
operator: In
values:
- myapp
topologyKey: kubernetes.io/hostnameChart Testing
values-test.yaml
# Test values
replicaCount: 1
image:
repository: myapp
tag: "test"
config:
logging:
level: DEBUG
postgresql:
enabled: true
auth:
postgresPassword: testPassword
password: testPassword
redis:
enabled: trueTest Templates
# templates/tests/test-connection.yaml
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "myapp.fullname" . }}-test-connection"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
spec:
restartPolicy: Never
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}/health']Helm Commands
Basic Commands
# Create a new chart
helm create mychart
# Validate chart
helm lint mychart/
# Dry run installation
helm install myrelease mychart/ --dry-run --debug
# Install chart
helm install myrelease mychart/
# Install with custom values
helm install myrelease mychart/ -f values-prod.yaml
# Install with overrides
helm install myrelease mychart/ --set replicaCount=5 --set image.tag=v2.0.0
# List releases
helm list
# Get release info
helm get values myrelease
helm get manifest myrelease
# Upgrade release
helm upgrade myrelease mychart/ -f values-prod.yaml
# Rollback release
helm rollback myrelease 1
# Uninstall release
helm uninstall myrelease
# Test release
helm test myreleasePackage and Repository
# Package chart
helm package mychart/
# Create repository index
helm repo index .
# Add repository
helm repo add myrepo https://charts.example.com
# Update repositories
helm repo update
# Search charts
helm search repo myapp
# Install from repository
helm install myrelease myrepo/myappChart Dependencies
Chart.yaml with Dependencies
apiVersion: v2
name: myapp
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
dependencies:
- name: postgresql
version: "11.6.12"
repository: "https://charts.bitnami.com/bitnami"
condition: postgresql.enabled
- name: redis
version: "16.9.11"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
- name: nginx-ingress
version: "4.1.0"
repository: "https://kubernetes.github.io/ingress-nginx"
condition: ingress.controller.enabledManaging Dependencies
# Download dependencies
helm dependency update
# Build dependencies
helm dependency build
# List dependencies
helm dependency listHooks
Pre-Install Hook
# templates/hooks/pre-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ include "myapp.fullname" . }}-pre-install"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
metadata:
name: "{{ include "myapp.fullname" . }}-pre-install"
spec:
restartPolicy: Never
containers:
- name: pre-install-job
image: busybox
command: ['sh', '-c', 'echo "Running pre-install setup..." && sleep 10']Post-Install Hook
# templates/hooks/post-install-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ include "myapp.fullname" . }}-post-install"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": post-install
"helm.sh/hook-weight": "1"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
template:
metadata:
name: "{{ include "myapp.fullname" . }}-post-install"
spec:
restartPolicy: Never
containers:
- name: post-install-job
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
command: ['sh', '-c', 'echo "Running database migrations..." && sleep 5']
env:
- name: DATABASE_URL
value: "postgresql://{{ include "myapp.databaseHost" . }}:5432/{{ .Values.config.database.name }}"Best Practices
1. Use Meaningful Names
# Good
{{- define "myapp.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 }}2. Validate Required Values
{{- if not .Values.database.host }}
{{- fail "Database host is required" }}
{{- end }}
# Or using required function
host: {{ required "Database host is required" .Values.database.host }}3. Use Checksums for ConfigMaps
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}4. Provide Sensible Defaults
# values.yaml
image:
repository: myapp
pullPolicy: IfNotPresent
tag: "" # Will use Chart.AppVersion if not set
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"5. Document Your Chart
# Chart.yaml
description: |
A comprehensive Helm chart for deploying myapp with:
- Automatic scaling support
- Multiple environment configurations
- Integrated monitoring
- Security best practicesThis comprehensive guide covers Helm Charts fundamentals, templating, dependencies, testing, and deployment strategies for Kubernetes applications.
