HELM COMPLETE TUTORIAL — DevOps Architect Level
1. Introduction to Helm
What is Helm?
Helm is a package manager for Kubernetes. Just like:
apt installs software on Ubuntu
npm installs packages for [Link]
pip installs libraries for Python
Helm installs applications on Kubernetes.
It takes your application — which might need 10 different Kubernetes resources — and lets you
install, upgrade, and remove everything as one single unit.
helm install my-app ./my-chart
helm upgrade my-app ./my-chart
helm rollback my-app
helm uninstall my-app
That's it. One command manages everything.
Why Helm is Needed — Real Problems It Solves
Problem 1 — Too many YAML files
Deploying even a simple app to Kubernetes requires multiple files:
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
[Link]
Without Helm you apply them one by one:
bash
kubectl apply -f [Link]
kubectl apply -f [Link]
kubectl apply -f [Link]
# ... and so on
With Helm:
bash
helm install my-app ./my-chart
# applies ALL files at once
Problem 2 — Copy-pasting for each environment
Without Helm, you keep separate copies of YAML for dev, staging, prod. When image tag changes
you edit every file in every environment.
dev/
[Link] ← replicas: 1, image: app:dev
staging/
[Link] ← replicas: 2, image: app:staging
production/
[Link] ← replicas: 5, image: app:v1.2.0
3 environments × 8 files = 24 YAML files to maintain.
With Helm, you have one chart and one values file per environment:
my-chart/ ← one chart
[Link] ← dev config
[Link] ← staging config
[Link] ← prod config
Change image tag in production:
bash
helm upgrade my-app ./my-chart \
-f [Link] \
--set [Link]=v1.3.0
Done. One command. One place.
Problem 3 — No rollback mechanism
With plain kubectl, if a deployment breaks:
bash
kubectl apply -f [Link] # broke something
# how do you go back? manually edit the file? find the old version in git?
With Helm:
bash
helm rollback my-app # instantly back to previous version
Helm tracks every version of your release automatically.
Problem 4 — No release tracking
With plain kubectl you have no idea:
What version is deployed right now?
When was it last changed?
What changed between deployments?
With Helm:
bash
helm history my-app
REVISION STATUS CHART DESCRIPTION
1 superseded my-app-1.0.0 Install complete
2 superseded my-app-1.0.0 Upgrade: image v1.1
3 deployed my-app-1.0.0 Upgrade: image v1.2
Full audit trail. Built in.
How Helm Compares to Plain Kubernetes YAML
Feature Plain YAML Helm
─────────────────────────────────────────────────────────
Install app kubectl apply helm install
Update app edit file+apply helm upgrade
Rollback manual/git helm rollback
Environment config duplicate files values files
Track deployments none helm history
Package and share zip files helm chart
Dependency mgmt manual [Link] deps
Template logic none Go templates
Plain YAML is fine for: learning Kubernetes, very simple single-resource deployments, one-off tasks.
Helm is needed for: real applications, multiple environments, teams, CI/CD pipelines, anything in
production.
2. Helm Basics
Installation — Step by Step
On Linux (most common for DevOps)
bash
# Method 1 — Official script (quickest)
curl [Link] | bash
# Method 2 — Manual (more control)
# Step 1: download the binary for your OS
wget [Link]
# Step 2: extract it
tar -zxvf [Link]
# Step 3: move to system path
mv linux-amd64/helm /usr/local/bin/helm
# Step 4: verify
helm version
# [Link]{Version:"v3.13.0", ...}
On Mac
bash
brew install helm
On Windows
bash
choco install kubernetes-helm
After installation — verify it works
bash
helm version
# Should print version info
helm help
# Shows all available commands
Helm CLI Commands — Every Command You Need
Think of these in groups:
Group 1 — Release management (most used)
bash
# Install a chart as a new release
helm install <release-name> <chart-path>
helm install my-app ./my-chart
helm install my-app ./my-chart -n production # in specific namespace
helm install my-app ./my-chart -f [Link] # with custom values
helm install my-app ./my-chart --set [Link]=v2.0 # override single value
# Upgrade an existing release
helm upgrade my-app ./my-chart
helm upgrade my-app ./my-chart -f [Link]
# Install if not exists, upgrade if exists (most used in CI/CD)
helm upgrade --install my-app ./my-chart -f [Link]
# Rollback to previous version
helm rollback my-app # one step back
helm rollback my-app 2 # specific revision number
# Uninstall a release
helm uninstall my-app
helm uninstall my-app -n production
Group 2 — Inspection commands
bash
# List all releases
helm list
helm list -n production
helm list --all-namespaces
# See release status
helm status my-app
# See release history
helm history my-app
# See values currently applied to a release
helm get values my-app
helm get values my-app --all # including defaults
# See all kubernetes manifests of a release
helm get manifest my-app
# See rendered templates WITHOUT installing (dry run)
helm install my-app ./my-chart --dry-run
helm install my-app ./my-chart --dry-run --debug # with extra detail
Group 3 — Chart development commands
bash
# Create a new chart (generates folder structure)
helm create my-chart
# Check chart for errors
helm lint ./my-chart
helm lint ./my-chart -f [Link]
# Render templates locally (no cluster needed)
helm template my-app ./my-chart
helm template my-app ./my-chart -f [Link]
# Package chart into .tgz file
helm package ./my-chart
# Update chart dependencies
helm dependency update ./my-chart
helm dependency build ./my-chart
Helm Repository — Add, Update, Search
A Helm repository is a place where charts are stored and shared. Think of it like npm registry but for
Helm charts.
Adding repositories
bash
# Add official stable charts
helm repo add stable [Link]
# Add bitnami (most popular community repo — has postgres, redis, etc.)
helm repo add bitnami [Link]
# Add ingress-nginx
helm repo add ingress-nginx [Link]
# Add cert-manager
helm repo add jetstack [Link]
# Add ArgoCD
helm repo add argo [Link]
Managing repositories
bash
# List all added repos
helm repo list
# Update repo index (like apt-get update)
helm repo update
# Remove a repo
helm repo remove bitnami
Searching charts
bash
# Search in all added repos
helm search repo postgresql
helm search repo bitnami/postgresql
# See all available versions
helm search repo bitnami/postgresql --versions
# Search on Artifact Hub (online search)
helm search hub postgresql
Installing from a repo
bash
# Install directly from repo
helm install my-postgres bitnami/postgresql
# Install specific version
helm install my-postgres bitnami/postgresql --version 12.1.0
# See what values a chart accepts before installing
helm show values bitnami/postgresql
# See chart information
helm show chart bitnami/postgresql
helm show readme bitnami/postgresql
3. Helm Chart Structure — Every File Explained
The Full Folder Structure
When you run helm create my-app, Helm generates this structure:
my-app/
├── [Link] ← Chart metadata
├── [Link] ← Default configuration values
├── .helmignore ← Files to ignore when packaging
├── charts/ ← Dependencies go here
└── templates/ ← Kubernetes manifest templates
├── [Link] ← Deployment template
├── [Link] ← Service template
├── [Link] ← Ingress template
├── [Link] ← HorizontalPodAutoscaler template
├── [Link] ← ServiceAccount template
├── [Link] ← ConfigMap (you add this)
├── _helpers.tpl ← Reusable template snippets
└── [Link] ← Message shown after install
Let's go through every single file.
[Link] — The Identity Card of Your Chart
This file describes your chart. Who made it, what version it is, what it does.
yaml
# [Link]
apiVersion: v2 # Always v2 for Helm 3. Don't change this.
name: my-app # Name of the chart.
# Used in resource names and labels.
description: A Helm chart for my web application
# Human-readable description.
type: application # Two options:
# 'application' = can be installed directly
# 'library' = only provides templates for others
version: 1.3.0 # CHART version. Bump this when the
# chart itself changes (template changes,
# new values added, etc.)
# Follows semver: [Link]
appVersion: "2.5.1" # APPLICATION version. What version of
# your actual app this chart deploys.
# Informational only — doesn't affect K8s.
keywords: # Optional — helps with search
- web
- api
maintainers: # Optional — who owns this chart
- name: Ali Hassan
email: ali@[Link]
dependencies: # Charts this chart depends on
- name: postgresql # dependency chart name
version: "12.1.0" # dependency version (use quotes)
repository: [Link]
condition: [Link] # only install if value is true
tags: # group dependencies with tags
- database
- name: redis
version: "17.3.0"
repository: [Link]
condition: [Link]
Key rule: When you change the chart templates or structure, bump version. When your app releases
a new version, update appVersion. They are independent.
[Link] — The Configuration File
This is the most important file for day-to-day usage. It contains all the default configuration values for
your chart.
Think of it as the control panel. Everything configurable about your app lives here.
yaml
# [Link] — complete example
# ── Replica count ──────────────────────────────────────
replicaCount: 2
# ── Container image ────────────────────────────────────
image:
repository: mycompany/my-app
tag: "1.0.0"
pullPolicy: IfNotPresent
# pullPolicy options:
# Always → always pull from registry
# IfNotPresent → use local if exists (recommended for prod)
# Never → never pull, must exist locally
# ── Service configuration ──────────────────────────────
service:
type: ClusterIP # ClusterIP, NodePort, LoadBalancer
port: 80 # port the service exposes
targetPort: 8080 # port your container listens on
# ── Ingress configuration ──────────────────────────────
ingress:
enabled: false # false by default, enable per environment
className: nginx
host: [Link]
tls:
enabled: false
secretName: myapp-tls
# ── Resource limits ────────────────────────────────────
resources:
requests:
cpu: 250m # minimum guaranteed CPU
memory: 256Mi # minimum guaranteed memory
limits:
cpu: 500m # maximum CPU allowed
memory: 512Mi # maximum memory (OOMKill if exceeded)
# ── Autoscaling ────────────────────────────────────────
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 70
# ── Environment variables ──────────────────────────────
env:
LOG_LEVEL: "info"
APP_ENV: "production"
PORT: "8080"
# ── ConfigMap data ─────────────────────────────────────
config:
database_host: "localhost"
cache_ttl: "300"
# ── Service account ────────────────────────────────────
serviceAccount:
create: true
name: "" # auto-generated if empty
annotations: {} # add AWS IRSA annotation here
# ── Pod annotations ────────────────────────────────────
podAnnotations: {}
# [Link]/scrape: "true"
# [Link]/port: "8080"
# ── Node selection ─────────────────────────────────────
nodeSelector: {}
tolerations: []
affinity: {}
# ── Health checks ──────────────────────────────────────
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
# ── Dependencies ───────────────────────────────────────
postgresql:
enabled: false # enable when needed
auth:
username: myapp
password: "" # never hardcode — pass via --set or CI/CD
database: myappdb
redis:
enabled: false
Golden rule: If something might need to change between environments, it goes in [Link].
Never hardcode environment-specific values in templates.
templates/ — Where Your Kubernetes Manifests Live
Every .yaml file in templates/ is a Kubernetes manifest with Go template syntax added. Helm reads
these files, substitutes the values, and produces plain Kubernetes YAML.
[Link] — The most important template
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
{{- if not .[Link] }}
replicas: {{ .[Link] }}
{{- end }}
selector:
matchLabels:
{{- include "[Link]" . | nindent 6 }}
template:
metadata:
labels:
{{- include "[Link]" . | nindent 8 }}
{{- with .[Link] }}
annotations:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
serviceAccountName: {{ include "[Link]" . }}
containers:
- name: {{ .[Link] }}
image: "{{ .[Link] }}:{{ .[Link] }}"
imagePullPolicy: {{ .[Link] }}
ports:
- containerPort: {{ .[Link] }}
protocol: TCP
{{- with .[Link] }}
livenessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
{{- with .[Link] }}
readinessProbe:
{{- toYaml . | nindent 12 }}
{{- end }}
resources:
{{- toYaml .[Link] | nindent 12 }}
env:
{{- range $key, $value := .[Link] }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "[Link]" . }}-config
{{- with .[Link] }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .[Link] }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
[Link]
yaml
apiVersion: v1
kind: Service
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
type: {{ .[Link] }}
ports:
- port: {{ .[Link] }}
targetPort: {{ .[Link] }}
protocol: TCP
name: http
selector:
{{- include "[Link]" . | nindent 4 }}
[Link] — with conditional block
yaml
{{- if .[Link] -}}
apiVersion: [Link]/v1
kind: Ingress
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
{{- if .[Link] }}
ingressClassName: {{ .[Link] }}
{{- end }}
{{- if .[Link] }}
tls:
- hosts:
- {{ .[Link] }}
secretName: {{ .[Link] }}
{{- end }}
rules:
- host: {{ .[Link] }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "[Link]" . }}
port:
number: {{ .[Link] }}
{{- end }}
Notice the {{- if .[Link] -}} wrapping the entire file. If [Link] is false, Helm
produces zero output for this file. The Ingress resource is simply not created.
[Link]
yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "[Link]" . }}-config
labels:
{{- include "[Link]" . | nindent 4 }}
data:
{{- range $key, $value := .[Link] }}
{{ $key }}: {{ $value | quote }}
{{- end }}
[Link]
yaml
{{- if .[Link] }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "[Link]" . }}
minReplicas: {{ .[Link] }}
maxReplicas: {{ .[Link] }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .[Link] }}
{{- end }}
[Link]
yaml
{{- if .[Link] -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
{{- with .[Link] }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
_helpers.tpl — Reusable Template Snippets
This is a special file. The underscore prefix tells Helm: "do not render this as a Kubernetes manifest."
It only defines reusable named templates that other files can call.
Think of it like a utility functions file in programming.
yaml
{{/*
──────────────────────────────────────────────
Expand the name of the chart.
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
{{- .[Link] | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
──────────────────────────────────────────────
Full name: combines release name + chart name
e.g. if release is "payments" and chart is "my-app"
result is "payments-my-app"
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
{{- if .[Link] }}
{{- .[Link] | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .[Link] .[Link] }}
{{- if contains $name .[Link] }}
{{- .[Link] | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .[Link] $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
──────────────────────────────────────────────
Common labels — applied to every resource
These are standard Kubernetes recommended labels
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
[Link]/chart: {{ printf "%s-%s" .[Link] .[Link] | replace "+" "_" | trunc 63 }}
{{ include "[Link]" . }}
{{- if .[Link] }}
[Link]/version: {{ .[Link] | quote }}
{{- end }}
[Link]/managed-by: {{ .[Link] }}
{{- end }}
{{/*
──────────────────────────────────────────────
Selector labels — used in matchLabels
These CANNOT change after first deployment
(changing them requires delete + recreate)
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
[Link]/name: {{ include "[Link]" . }}
[Link]/instance: {{ .[Link] }}
{{- end }}
{{/*
──────────────────────────────────────────────
Service account name logic
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
{{- if .[Link] }}
{{- default (include "[Link]" .) .[Link] }}
{{- else }}
{{- default "default" .[Link] }}
{{- end }}
{{- end }}
{{/*
──────────────────────────────────────────────
Database URL constructor
Builds connection string from values
──────────────────────────────────────────────
*/}}
{{- define "[Link]" -}}
{{- printf "postgresql://%s:%s@%s:5432/%s"
.[Link]
.[Link]
(include "[Link]" .)
.[Link]
-}}
{{- end }}
How to call a helper in another template
yaml
# In [Link]
name: {{ include "[Link]" . }}
# ↑ "include" keyword
# ↑ name of defined template
# ↑ dot = current context (pass everything)
[Link] — Post-Install Message
This file is rendered and printed to the user's terminal after helm install or helm upgrade. It is a
template too — you can use values in it.
Thank you for installing {{ .[Link] }} version {{ .[Link] }}!
Release name: {{ .[Link] }}
Namespace: {{ .[Link] }}
Your application is now running.
{{- if .[Link] }}
Access it at: [Link] .[Link] }}
{{- else }}
To access locally, run:
kubectl port-forward svc/{{ include "[Link]" . }} 8080:{{ .[Link] }}
Then open: [Link]
{{- end }}
To see deployment status:
helm status {{ .[Link] }}
To see logs:
kubectl logs -l [Link]/instance={{ .[Link] }} -f
4. Creating Your First Helm Chart — Step by Step
Step 1 — Generate the chart scaffold
bash
helm create webapp
This creates:
webapp/
├── [Link]
├── [Link]
├── charts/
└── templates/
├── [Link]
├── [Link]
├── [Link]
├── [Link]
├── [Link]
├── _helpers.tpl
└── [Link]
Step 2 — Update [Link]
yaml
apiVersion: v2
name: webapp
description: A simple web application
type: application
version: 1.0.0
appVersion: "1.0.0"
Step 3 — Define your [Link]
yaml
replicaCount: 1
image:
repository: nginx
tag: "1.25"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 80
ingress:
enabled: false
host: [Link]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
targetCPUUtilizationPercentage: 80
env:
APP_ENV: production
serviceAccount:
create: true
name: ""
annotations: {}
Step 4 — Write the deployment template
yaml
# templates/[Link]
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
{{- if not .[Link] }}
replicas: {{ .[Link] }}
{{- end }}
selector:
matchLabels:
{{- include "[Link]" . | nindent 6 }}
template:
metadata:
labels:
{{- include "[Link]" . | nindent 8 }}
spec:
serviceAccountName: {{ include "[Link]" . }}
containers:
- name: webapp
image: "{{ .[Link] }}:{{ .[Link] }}"
imagePullPolicy: {{ .[Link] }}
ports:
- containerPort: {{ .[Link] }}
resources:
{{- toYaml .[Link] | nindent 12 }}
env:
{{- range $key, $value := .[Link] }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
Step 5 — Lint and test it
bash
# Check for errors
helm lint ./webapp
# See what would be deployed
helm template my-webapp ./webapp
# Dry run against real cluster
helm install my-webapp ./webapp --dry-run --debug
Step 6 — Install it
bash
# Install
helm install my-webapp ./webapp
# Verify
helm list
kubectl get pods
kubectl get svc
Step 7 — Upgrade it
Change image tag in [Link] to "1.26", then:
bash
helm upgrade my-webapp ./webapp
# or override on command line:
helm upgrade my-webapp ./webapp --set [Link]=1.26
Step 8 — Clean up
bash
helm uninstall my-webapp
5. Templates and Values — Core Concepts
Go Templating — Simple Explanation
Helm uses Go's template engine. The syntax looks scary at first but follows simple rules.
The basic rule: anything inside {{ }} is processed by Helm. Everything outside is plain text/YAML.
yaml
# Plain YAML (outside double braces)
kind: Deployment
metadata:
# Processed by Helm (inside double braces)
name: {{ .[Link] }}
The Dot — Most Important Concept
The dot . represents the current context — the object you're working with. At the top level it
contains everything:
.Values → everything in [Link]
.Release → information about this release
.Chart → information from [Link]
.Files → access to non-template files
.Capabilities → Kubernetes cluster capabilities
yaml
{{ .[Link] }} # access nested values
{{ .[Link] }} # release name (given at helm install)
{{ .[Link] }} # kubernetes namespace
{{ .[Link] }} # chart version
{{ .[Link] }} # app version
Using [Link] — How Values Flow
yaml
# [Link]
image:
repository: myapp
tag: "2.0"
replicaCount: 3
yaml
# template file
spec:
replicas: {{ .[Link] }}
# becomes → replicas: 3
containers:
- image: "{{ .[Link] }}:{{ .[Link] }}"
# becomes → image: "myapp:2.0"
Passing Values Dynamically
You can override values at deploy time in multiple ways:
bash
# Method 1 — --set for single values (dot notation for nesting)
helm install my-app ./my-chart --set [Link]=v2.0
helm install my-app ./my-chart --set replicaCount=5
helm install my-app ./my-chart --set [Link]=LoadBalancer
# Method 2 — --set for multiple values
helm install my-app ./my-chart \
--set [Link]=v2.0 \
--set replicaCount=5 \
--set [Link]=true
# Method 3 — -f for a values file
helm install my-app ./my-chart -f [Link]
# Method 4 — multiple files (merged in order, last wins)
helm install my-app ./my-chart \
-f [Link] \
-f [Link] \
--set [Link]=v2.0
Values precedence (highest to lowest)
--set flag ← wins over everything
--set-string flag
--set-file flag
-f custom values file
[Link] in chart ← lowest priority (defaults)
Conditional Logic — if, else, else if
Basic if
yaml
# Only create this block if ingress is enabled
{{- if .[Link] }}
apiVersion: [Link]/v1
kind: Ingress
...
{{- end }}
if with else
yaml
spec:
type: {{ .[Link] }}
{{- if eq .[Link] "NodePort" }}
ports:
- port: {{ .[Link] }}
nodePort: {{ .[Link] }}
{{- else }}
ports:
- port: {{ .[Link] }}
{{- end }}
if, else if, else
yaml
{{- if eq .[Link] "production" }}
replicas: 5
{{- else if eq .[Link] "staging" }}
replicas: 2
{{- else }}
replicas: 1
{{- end }}
Comparison operators
yaml
eq → equal {{ if eq .[Link] "prod" }}
ne → not equal {{ if ne .[Link] "dev" }}
lt → less than {{ if lt .[Link] 3 }}
gt → greater than {{ if gt .[Link] 1 }}
le → less or equal {{ if le .[Link] 5 }}
ge → greater/equal {{ if ge .[Link] 2 }}
Boolean operators
yaml
and → {{ if and .[Link] .[Link] }}
or → {{ if or .[Link] .[Link] }}
not → {{ if not .[Link] }}
Checking if value exists / is not empty
yaml
{{- if .[Link] }}
annotations:
{{- toYaml .[Link] | nindent 4 }}
{{- end }}
# with does the same + changes context (dot)
{{- with .[Link] }}
annotations:
{{- toYaml . | nindent 4 }}
# ↑ dot now refers to .[Link]
{{- end }}
Loops — range
Loop over a list
yaml
# [Link]
tolerations:
- key: node-role
operator: Equal
value: worker
effect: NoSchedule
- key: dedicated
operator: Equal
value: gpu
effect: NoSchedule
yaml
# template
tolerations:
{{- range .[Link] }}
- key: {{ .key }}
operator: {{ .operator }}
value: {{ .value }}
effect: {{ .effect }}
{{- end }}
Loop over a map (key-value pairs)
yaml
# [Link]
env:
APP_ENV: production
LOG_LEVEL: info
PORT: "8080"
yaml
# template
env:
{{- range $key, $value := .[Link] }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
Produces:
yaml
env:
- name: APP_ENV
value: "production"
- name: LOG_LEVEL
value: "info"
- name: PORT
value: "8080"
Loop with index
yaml
{{- range $index, $item := .[Link] }}
- name: server-{{ $index }}
address: {{ $[Link] }}
{{- end }}
Pipes and Functions — Essential List
yaml
# quote — wrap value in quotes (always use for string env vars)
value: {{ .[Link] | quote }}
# "8080" ← with quotes
# default — use fallback if value is empty or not set
name: {{ .[Link] | default .[Link] }}
# upper / lower — change case
label: {{ .[Link] | upper }}
# PRODUCTION
# trunc — truncate to max length
# (Kubernetes names max 63 characters)
name: {{ .[Link] | trunc 63 | trimSuffix "-" }}
# replace — string replacement
{{ .[Link] | replace "+" "_" }}
# printf — format string (like sprintf)
name: {{ printf "%s-%s" .[Link] .[Link] }}
# nindent — add newline then indent N spaces
# (critical for nested YAML blocks)
labels:
{{- include "[Link]" . | nindent 4 }}
# toYaml — convert a values block to YAML
resources:
{{- toYaml .[Link] | nindent 12 }}
# b64enc — base64 encode (for secrets)
password: {{ .[Link] | b64enc }}
# required — fail with message if value missing
image: {{ required "[Link] is required!" .[Link] }}
# contains — check if string contains substring
{{- if contains "prod" .[Link] }}
The Whitespace Problem — Dashes Explained
This is where beginners get confused. YAML is whitespace-sensitive. Wrong indentation breaks
everything.
yaml
# WITHOUT dashes — keeps all whitespace
name: {{ .[Link] }}
# produces:
name: myapp
# ← fine
# The problem with blocks:
metadata:
labels:
{{ include "[Link]" . | nindent 4 }}
# ← extra blank line before labels = invalid YAML
# WITH leading dash — removes whitespace BEFORE the tag
metadata:
labels:
{{- include "[Link]" . | nindent 4 }}
# ← no extra blank line = valid YAML
Simple rule: Use {{- (leading dash) to remove whitespace before your template tag. Use -}} (trailing
dash) to remove whitespace after. When in doubt, use {{- on most lines.
6. Managing Releases
Install, Upgrade, Rollback, Uninstall — Full Lifecycle
Install
bash
# Basic install
helm install my-app ./my-chart
# With namespace (create namespace if not exists)
helm install my-app ./my-chart \
--namespace production \
--create-namespace
# With values file
helm install my-app ./my-chart \
-f [Link]
# With timeout (wait up to 5 min for pods to be ready)
helm install my-app ./my-chart \
--wait \
--timeout 5m
# Atomic — rollback automatically if install fails
helm install my-app ./my-chart \
--atomic \
--timeout 5m
Upgrade
bash
# Basic upgrade
helm upgrade my-app ./my-chart
# With new values
helm upgrade my-app ./my-chart \
-f [Link] \
--set [Link]=v2.0
# Most used in CI/CD — install if not exists, upgrade if exists
helm upgrade --install my-app ./my-chart \
-f [Link] \
--atomic \
--timeout 5m
# Reuse existing values (don't reset to defaults)
helm upgrade my-app ./my-chart --reuse-values
# Force recreate pods even if no change detected
helm upgrade my-app ./my-chart --force
Rollback
bash
# See history first
helm history my-app
# REVISION STATUS CHART DESCRIPTION
#1 superseded my-app-1.0.0 Install complete
#2 superseded my-app-1.0.0 Upgrade complete
#3 deployed my-app-1.0.0 Upgrade: set [Link]=v3.0
# Rollback to previous revision (revision 2)
helm rollback my-app
# Rollback to specific revision
helm rollback my-app 1
# Rollback and wait for pods to be ready
helm rollback my-app --wait
# Rollback with timeout
helm rollback my-app --wait --timeout 5m
Uninstall
bash
# Remove all resources created by this release
helm uninstall my-app
# In specific namespace
helm uninstall my-app -n production
# Keep history (for audit trail)
helm uninstall my-app --keep-history
Versioning Concepts
Helm has two separate versions to understand:
Chart Version (in [Link] → version field)
→ Tracks changes to the chart itself
→ Bump when you: add new template, add new value, fix template bug
→ Follows semver: 1.0.0 → 1.0.1 (patch fix) → 1.1.0 (new feature) → 2.0.0 (breaking)
App Version (in [Link] → appVersion field)
→ Tracks which version of your application is being deployed
→ Usually matches your Docker image tag
→ Informational — doesn't affect Kubernetes resources directly
yaml
# [Link]
version: 2.1.0 # chart changed significantly (new values, template fixes)
appVersion: "5.3.1" # application v5.3.1 is what this deploys
Release Revision — every time you install or upgrade, Helm increments the revision:
helm install → revision 1
helm upgrade → revision 2
helm upgrade → revision 3
helm rollback → revision 4 (rollback creates NEW revision, doesn't delete old)
Real-World Deployment Workflow
This is how a production team actually works with Helm day to day:
bash
# ─── Developer makes code change ───────────────────────────────
# Step 1: Build and push new Docker image
docker build -t mycompany/my-app:v2.3.1 .
docker push mycompany/my-app:v2.3.1
# Step 2: Update chart version if templates changed
# (edit [Link]: version: 1.2.0 → 1.2.1)
# ─── CI/CD pipeline runs ───────────────────────────────────────
# Step 3: Lint the chart
helm lint ./my-chart -f [Link]
# Step 4: See what would change (like terraform plan)
helm diff upgrade my-app ./my-chart \
-f [Link] \
--set [Link]=v2.3.1
# Step 5: Deploy to staging first
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]=v2.3.1 \
--atomic \
--timeout 5m \
-n staging
# Step 6: Run smoke tests against staging
# (your test script)
./[Link] staging
# Step 7: Deploy to production (with manual approval gate)
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]=v2.3.1 \
--atomic \
--timeout 10m \
-n production
# ─── Something goes wrong ──────────────────────────────────────
# Step 8: Rollback immediately
helm rollback my-app -n production
# Step 9: Verify rollback
helm status my-app -n production
kubectl get pods -n production
7. Dependency Management
What Are Dependencies?
Your application needs a database. Your database needs Helm chart. Instead of managing it
separately, declare it as a dependency in your chart. When you install your chart, the database
installs automatically.
yaml
# [Link]
dependencies:
- name: postgresql # chart name in the repo
version: "12.1.0" # exact version (always pin, never use *)
repository: [Link]
condition: [Link] # only install if value is true
- name: redis
version: "17.3.0"
repository: [Link]
condition: [Link]
- name: rabbitmq
version: "11.0.0"
repository: [Link]
condition: [Link]
How to Use Dependencies
bash
# Step 1: Add the repositories your dependencies need
helm repo add bitnami [Link]
helm repo update
# Step 2: Download dependencies into charts/ folder
helm dependency update ./my-chart
# This creates:
# charts/[Link]
# charts/[Link]
# [Link] ← exact versions locked (like [Link])
# Step 3: Now install — dependencies install automatically
helm install my-app ./my-chart
Configuring Dependencies via [Link]
The dependency chart's values are configured under a key matching the dependency name:
yaml
# [Link]
# Your app values
replicaCount: 2
# PostgreSQL dependency configuration
# (these are postgresql chart's own values — nested under "postgresql")
postgresql:
enabled: true
auth:
username: myapp
password: "changeme123" # in prod: pass via --set or CI secret
database: myappdb
primary:
persistence:
enabled: true
size: 20Gi
resources:
requests:
cpu: 500m
memory: 512Mi
# Redis dependency configuration
redis:
enabled: true
auth:
enabled: false # no password for internal redis
master:
persistence:
enabled: false # no persistence needed for cache
replica:
replicaCount: 0 # no replicas in dev
Connecting Your App to the Dependency
The dependency creates a service inside the cluster. You access it by service name:
yaml
# Service name pattern for dependencies:
# <release-name>-<dependency-name>
# e.g. if release is "my-app", postgresql service is "my-app-postgresql"
# [Link]
env:
DATABASE_HOST: "{{ .[Link] }}-postgresql"
DATABASE_PORT: "5432"
DATABASE_NAME: "myappdb"
yaml
# Better: use _helpers.tpl to build the URL
{{- define "[Link]" -}}
{{- printf "%s-postgresql" .[Link] }}
{{- end }}
Local Chart Dependencies
For charts in your own organisation:
yaml
# [Link]
dependencies:
- name: common-lib
version: "1.0.0"
repository: "[Link] # local path
# OR from your private registry:
# repository: "[Link]
[Link] — Never Edit This Manually
After helm dependency update, Helm creates [Link]:
yaml
# [Link] — auto-generated, commit to git
dependencies:
- name: postgresql
repository: [Link]
version: 12.1.0
- name: redis
repository: [Link]
version: 17.3.0
digest: sha256:abc123...
generated: "2024-01-15T10:30:00Z"
This ensures everyone on your team gets exactly the same dependency version. Commit [Link]
to git, just like [Link].
8. Helm with Kubernetes — Practical Usage
Deploying Real Applications
ConfigMaps in Helm
ConfigMaps store non-sensitive configuration. Two patterns:
Pattern 1 — Values-driven ConfigMap
yaml
# [Link]
appConfig:
database_pool_size: "10"
cache_ttl: "300"
feature_flag_new_ui: "false"
log_format: "json"
yaml
# templates/[Link]
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "[Link]" . }}-config
labels:
{{- include "[Link]" . | nindent 4 }}
data:
{{- range $key, $value := .[Link] }}
{{ $key }}: {{ $value | quote }}
{{- end }}
Pattern 2 — File-based ConfigMap (for config files)
yaml
# templates/[Link]
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "[Link]" . }}-config
data:
[Link]: |
server {
listen {{ .[Link] }};
server_name {{ .[Link] }};
location /health {
return 200 'healthy';
location / {
proxy_pass [Link] .[Link] }};
[Link]: |
[Link]={{ .[Link].database_pool_size }}
[Link]={{ .[Link].cache_ttl }}
Secrets in Helm
yaml
# templates/[Link]
apiVersion: v1
kind: Secret
metadata:
name: {{ include "[Link]" . }}-secret
labels:
{{- include "[Link]" . | nindent 4 }}
type: Opaque
data:
# b64enc encodes to base64 — required by Kubernetes Secrets
database-url: {{ .[Link] | b64enc | quote }}
api-key: {{ .[Link] | b64enc | quote }}
yaml
# [Link] — placeholders only (real values passed at deploy time)
secrets:
databaseUrl: "" # pass via: --set [Link]=$DB_URL
apiKey: "" # pass via: --set [Link]=$API_KEY
bash
# Deploy with secrets from environment variables (CI/CD)
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]="$DATABASE_URL" \
--set [Link]="$API_KEY"
Mounting ConfigMap and Secret in Deployment
yaml
# templates/[Link]
spec:
containers:
- name: {{ .[Link] }}
image: "{{ .[Link] }}:{{ .[Link] }}"
# Mount ConfigMap as environment variables
envFrom:
- configMapRef:
name: {{ include "[Link]" . }}-config
# Mount specific Secret values as env vars
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "[Link]" . }}-secret
key: database-url
- name: API_KEY
valueFrom:
secretKeyRef:
name: {{ include "[Link]" . }}-secret
key: api-key
# Mount ConfigMap as files
volumeMounts:
- name: app-config
mountPath: /etc/app/config
readOnly: true
volumes:
- name: app-config
configMap:
name: {{ include "[Link]" . }}-config
Environment-Specific Configuration
This is one of the most important real-world patterns.
File structure
my-chart/
├── [Link]
├── [Link] ← shared defaults (base)
├── [Link] ← dev overrides
├── [Link] ← staging overrides
└── [Link] ← production overrides
[Link] (base defaults)
yaml
replicaCount: 1
image:
repository: mycompany/my-app
tag: "latest"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: false
host: ""
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 5
appConfig:
log_level: "debug"
cache_ttl: "60"
postgresql:
enabled: false
[Link] (only what's different)
yaml
# DEV — minimal resources, debug logging, no HA
replicaCount: 1
image:
tag: "dev"
appConfig:
log_level: "debug"
cache_ttl: "10"
postgresql:
enabled: true
auth:
username: devuser
password: devpassword
database: devdb
primary:
persistence:
size: 1Gi
[Link]
yaml
# STAGING — close to production, smaller scale
replicaCount: 2
image:
tag: "staging"
ingress:
enabled: true
host: [Link]
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: 400m
memory: 512Mi
appConfig:
log_level: "info"
cache_ttl: "120"
postgresql:
enabled: true
auth:
username: staginguser
password: "" # passed via CI --set
database: stagingdb
[Link]
yaml
# PRODUCTION — full resources, HA, autoscaling
replicaCount: 5
image:
tag: "1.5.0" # always pinned version in prod
pullPolicy: Always
ingress:
enabled: true
host: [Link]
tls:
enabled: true
secretName: myapp-tls-cert
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
targetCPUUtilizationPercentage: 70
appConfig:
log_level: "warn"
cache_ttl: "300"
postgresql:
enabled: true
auth:
username: produser
password: "" # NEVER in file — passed via CI secret
database: proddb
primary:
persistence:
size: 100Gi
resources:
requests:
cpu: 1000m
memory: 2Gi
Deploying per environment
bash
# Dev
helm upgrade --install my-app ./my-chart \
-f [Link] \
-n dev --create-namespace
# Staging
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]="$STAGING_DB_PASS" \
-n staging
# Production
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]="$RELEASE_TAG" \
--set [Link]="$PROD_DB_PASS" \
--atomic --timeout 10m \
-n production
9. Helm Best Practices — Architect Level
Chart Design Principles
1. Sensible defaults that work out of the box
yaml
# BAD — user must set everything or it fails
image:
repository: "" # required but no default
tag: "" # required but no default
# GOOD — works with defaults, can be overridden
image:
repository: mycompany/my-app
tag: "latest"
2. Feature flags with conditions — don't create unused resources
yaml
# Every optional component should have an enabled flag
ingress:
enabled: false # off by default, on in prod
autoscaling:
enabled: false # off by default, on in prod
monitoring:
enabled: false # off by default, on in prod
3. Required values should fail loudly
yaml
# templates/[Link]
image: "{{ required "ERROR: [Link] must be set" .[Link] }}:{{ required
"ERROR: [Link] must be set" .[Link] }}"
If someone installs without setting these, they get a clear error message immediately.
4. Never hardcode anything that changes per environment
yaml
# BAD — hardcoded in template
replicas: 3
image: myapp:v2.0
# GOOD — comes from values
replicas: {{ .[Link] }}
image: "{{ .[Link] }}:{{ .[Link] }}"
5. Labels — use standard Kubernetes labels everywhere
yaml
# Standard recommended labels — use in every resource
labels:
[Link]/name: {{ include "[Link]" . }}
[Link]/instance: {{ .[Link] }}
[Link]/version: {{ .[Link] | quote }}
[Link]/managed-by: {{ .[Link] }}
[Link]/chart: {{ printf "%s-%s" .[Link] .[Link] }}
Reusability and Modularity
Library charts — define once, use in 50 microservices
yaml
# common-lib/[Link]
type: library # cannot be installed directly
name: common-lib
version: 1.0.0
yaml
# common-lib/templates/_deployment.tpl
{{- define "[Link]" -}}
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
replicas: {{ .[Link] | default 1 }}
selector:
matchLabels:
{{- include "[Link]" . | nindent 6 }}
template:
metadata:
labels:
{{- include "[Link]" . | nindent 8 }}
spec:
containers:
- name: {{ .[Link] }}
image: "{{ .[Link] }}:{{ .[Link] }}"
resources:
{{- toYaml .[Link] | nindent 12 }}
{{- end }}
yaml
# any-microservice/[Link]
dependencies:
- name: common-lib
version: "1.0.0"
repository: "[Link]
yaml
# any-microservice/templates/[Link]
# Just one line — entire deployment comes from library
{{ include "[Link]" . }}
Versioning Strategy
Patch (1.0.0 → 1.0.1): bug fix in a template, no new values
Minor (1.0.0 → 1.1.0): new optional values added, backward compatible
Major (1.0.0 → 2.0.0): breaking change — renamed values, removed values
bash
# Tag chart versions in git
git tag chart/my-app/v1.2.0
git push origin chart/my-app/v1.2.0
Production Safety Rules
bash
# Rule 1 — Always use --atomic in production
helm upgrade --install my-app ./my-chart --atomic --timeout 10m
# Auto-rollback if deployment fails
# Rule 2 — Limit history to avoid etcd memory bloat
helm upgrade my-app ./my-chart --history-max 10
# Keep only last 10 revisions
# Rule 3 — Never use image tag "latest" in production
# BAD
image:
tag: latest
# GOOD
image:
tag: "1.5.2" # exact version, reproducible
# Rule 4 — Validate before every deployment
helm lint ./my-chart -f [Link]
helm template my-app ./my-chart -f [Link] \
| kubectl apply --dry-run=client -f -
# Rule 5 — Use --wait to ensure pods are healthy
helm upgrade --install my-app ./my-chart --wait --timeout 10m
# Helm waits until pods are Running + Ready before reporting success
10. Helm + CI/CD Integration
How Helm Fits in a Pipeline
Code Push → CI runs tests → Build Docker image →
Push image to registry → Helm upgrade in cluster →
Smoke tests → Done
GitLab CI Pipeline — Complete Example
yaml
# .[Link]
variables:
APP_NAME: my-app
CHART_PATH: ./helm/my-app
REGISTRY: [Link]/mycompany
stages:
- test
- build
- validate
- deploy-staging
- deploy-production
# ── Test stage ──────────────────────────────────────────────────
unit-tests:
stage: test
image: node:18
script:
- npm ci
- npm test
only:
- merge_requests
- main
# ── Build stage ─────────────────────────────────────────────────
build-image:
stage: build
image: docker:24
services:
- docker:dind
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $REGISTRY/$APP_NAME:$CI_COMMIT_SHORT_SHA .
- docker push $REGISTRY/$APP_NAME:$CI_COMMIT_SHORT_SHA
only:
- main
- tags
# ── Validate stage ──────────────────────────────────────────────
helm-lint:
stage: validate
image: alpine/helm:3.13.0
script:
- helm lint $CHART_PATH -f $CHART_PATH/[Link]
only:
- merge_requests
- main
helm-diff-staging:
stage: validate
image: alpine/helm:3.13.0
script:
- helm plugin install [Link]
- helm diff upgrade $APP_NAME $CHART_PATH
-f $CHART_PATH/[Link]
--set [Link]=$CI_COMMIT_SHORT_SHA
-n staging
only:
- main
# ── Deploy staging ──────────────────────────────────────────────
deploy-staging:
stage: deploy-staging
image: alpine/helm:3.13.0
script:
- helm upgrade --install $APP_NAME $CHART_PATH
-f $CHART_PATH/[Link]
--set [Link]=$CI_COMMIT_SHORT_SHA
--set [Link]=$REGISTRY/$APP_NAME
--atomic
--timeout 5m
--history-max 10
-n staging
--create-namespace
environment:
name: staging
url: [Link]
only:
- main
smoke-test-staging:
stage: deploy-staging
image: curlimages/curl
script:
- sleep 10
- curl -f [Link]
- curl -f [Link]
needs: [deploy-staging]
only:
- main
# ── Deploy production ───────────────────────────────────────────
deploy-production:
stage: deploy-production
image: alpine/helm:3.13.0
script:
- helm upgrade --install $APP_NAME $CHART_PATH
-f $CHART_PATH/[Link]
--set [Link]=$CI_COMMIT_TAG
--set [Link]=$REGISTRY/$APP_NAME
--set [Link]=$PROD_DB_PASSWORD
--atomic
--timeout 10m
--history-max 10
-n production
--create-namespace
environment:
name: production
url: [Link]
when: manual # human must approve
only:
- tags # only deploy tagged releases
GitOps with ArgoCD — Overview
GitOps means: Git is the source of truth for what's deployed. ArgoCD watches Git and makes the
cluster match it automatically.
Developer pushes Helm values change to Git
ArgoCD detects the change (polls every 3 min or via webhook)
ArgoCD runs helm template to render manifests
ArgoCD compares rendered manifests to what's in cluster
ArgoCD applies the diff (helm upgrade equivalent)
Cluster matches Git
ArgoCD Application using a Helm chart
yaml
# [Link]
apiVersion: [Link]/v1alpha1
kind: Application
metadata:
name: my-app-production
namespace: argocd
spec:
project: default
source:
repoURL: [Link]
targetRevision: main # git branch or tag
path: charts/my-app # path to chart in repo
helm:
valueFiles:
- [Link]
- [Link]
parameters:
- name: [Link]
value: "1.5.0"
destination:
server: [Link]
namespace: production
syncPolicy:
automated:
prune: true # delete K8s resources removed from chart
selfHeal: true # fix manual changes to cluster (drift correction)
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
GitOps Repository Structure
gitops-repo/
├── apps/
│ ├── my-app/
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ └── another-app/
│ └── ...
└── argocd/
├── [Link] ← ArgoCD Application for staging
└── [Link] ← ArgoCD Application for production
11. Debugging and Troubleshooting
Debug Commands — Your Toolkit
bash
# ─── Before deploying ──────────────────────────────────────────
# Check chart for syntax errors
helm lint ./my-chart
helm lint ./my-chart -f [Link]
# Render templates locally (no cluster needed)
# Shows EXACTLY what YAML would be applied
helm template my-app ./my-chart -f [Link]
# Render and check with kubectl (without applying)
helm template my-app ./my-chart -f [Link] \
| kubectl apply --dry-run=client -f -
# ─── After deploying ──────────────────────────────────────────
# Check release status
helm status my-app -n production
# See what values are currently live
helm get values my-app -n production
helm get values my-app -n production --all # with defaults
# See the actual manifests that were applied
helm get manifest my-app -n production
# See all notes (post-install messages)
helm get notes my-app -n production
# Full release info
helm get all my-app -n production
Common Problems and Solutions
Problem 1 — "Release already exists"
bash
Error: INSTALLATION FAILED: cannot re-use a name that is still in use
Solution:
bash
# Option A — upgrade instead of install
helm upgrade --install my-app ./my-chart # idempotent
# Option B — uninstall first
helm uninstall my-app
helm install my-app ./my-chart
Problem 2 — "Rendered manifests contain a resource that already exists"
bash
Error: rendered manifests contain a resource that already
exists. Unable to continue with install: ServiceAccount
"my-app" already exists in namespace "production"
Solution:
bash
# Option A — use --force to overwrite
helm install my-app ./my-chart --force
# Option B — annotate existing resource so Helm adopts it
kubectl annotate serviceaccount my-app \
[Link]/release-name=my-app \
[Link]/release-namespace=production
kubectl label serviceaccount my-app \
[Link]/managed-by=Helm
Problem 3 — Pods not starting after deploy
bash
# Step 1: check release status
helm status my-app
# Step 2: see what Helm deployed
helm get manifest my-app | grep -A 20 "kind: Deployment"
# Step 3: check pod status
kubectl get pods -n production -l [Link]/instance=my-app
# Step 4: describe the failing pod
kubectl describe pod <pod-name> -n production
# Look at Events section at the bottom
# Step 5: check logs
kubectl logs <pod-name> -n production
kubectl logs <pod-name> -n production --previous # if pod crashed
# Common causes:
# - Image pull error: wrong image name/tag, no registry credentials
# - CrashLoopBackOff: app is crashing — check logs
# - OOMKilled: memory limit too low — increase limits
# - Pending: not enough node resources — check node capacity
Problem 4 — Values not being applied
bash
# Check what values Helm is actually using
helm get values my-app --all
# Re-render templates with your values file to see output
helm template my-app ./my-chart -f [Link] \
| grep -A 5 "image:"
# Ensure your values file is valid YAML
python3 -c "import yaml; yaml.safe_load(open('[Link]'))"
Problem 5 — Upgrade fails, release stuck in "pending-upgrade"
bash
# Check status
helm list -n production
# STATUS: pending-upgrade ← stuck
# Option A — force a rollback
helm rollback my-app -n production
# Option B — if rollback fails, manually fix the release secret
kubectl get secret -n production | grep my-app
# Helm stores state in secrets named: [Link]-app.v3
# Last resort — delete and reinstall
helm uninstall my-app --keep-history -n production
helm install my-app ./my-chart -f [Link] -n production
Problem 6 — Template rendering error
bash
Error: template: my-app/templates/[Link]:18:
executing "my-app/templates/[Link]" at <.[Link]>:
nil pointer evaluating interface {}.tag
Solution:
bash
# 1. Find line 34 in [Link]
# 2. Check [Link] has the expected structure
# 3. Run with debug flag for more info
helm install my-app ./my-chart --dry-run --debug 2>&1 | head -80
Problem 7 — YAML indentation error
bash
Error: YAML parse error on my-app/templates/[Link]:
error converting YAML to JSON: yaml: line 45: did not find expected key
Solution:
bash
# Render template and validate YAML
helm template my-app ./my-chart | python3 -c "
import sys, yaml
docs = list(yaml.safe_load_all([Link]))
print(f'OK: {len(docs)} documents parsed')
"
12. Advanced Concepts — Architect Level
Helm Hooks — Run Jobs at Specific Points
Hooks are Kubernetes resources (usually Jobs) that run at specific points in the Helm lifecycle.
pre-install → runs BEFORE any chart resources are created
post-install → runs AFTER all chart resources are created
pre-upgrade → runs BEFORE upgrade starts
post-upgrade → runs AFTER upgrade completes
pre-rollback → runs BEFORE rollback
post-rollback → runs AFTER rollback
pre-delete → runs BEFORE uninstall
post-delete → runs AFTER uninstall
test → runs when you execute: helm test my-app
Database migration hook — real-world example
yaml
# templates/[Link]
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "[Link]" . }}-migration
labels:
{{- include "[Link]" . | nindent 4 }}
annotations:
# This makes it a hook
"[Link]/hook": pre-install,pre-upgrade
# Run migrations BEFORE both install and upgrade
"[Link]/hook-weight": "-5"
# Order among hooks. Lower number runs first.
# -5 runs before 0, which runs before 5
"[Link]/hook-delete-policy": before-hook-creation,hook-succeeded
# before-hook-creation → delete old job before creating new
# hook-succeeded → delete job after it succeeds
# hook-failed → delete job if it fails (for cleanup)
spec:
backoffLimit: 3 # retry 3 times before marking failed
template:
spec:
restartPolicy: Never
serviceAccountName: {{ include "[Link]" . }}
containers:
- name: migration
image: "{{ .[Link] }}:{{ .[Link] }}"
command: ["/bin/sh", "-c"]
args:
-|
echo "Running database migrations..."
./[Link]
echo "Migrations complete."
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: {{ include "[Link]" . }}-secret
key: database-url
Test hook
yaml
# templates/tests/[Link]
apiVersion: v1
kind: Pod
metadata:
name: {{ include "[Link]" . }}-test-connection
annotations:
"[Link]/hook": test
"[Link]/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
restartPolicy: Never
containers:
- name: test
image: curlimages/curl:latest
command: ["curl", "-f"]
args:
- "[Link] include "[Link]" . }}:{{ .[Link] }}/health"
bash
# Run tests after install
helm test my-app -n production
# Executes the test pod and reports pass/fail
Helm Plugins — Extend Helm's Capabilities
bash
# Install a plugin
helm plugin install <URL>
# List installed plugins
helm plugin list
# Remove a plugin
helm plugin remove <plugin-name>
Essential plugins for production
bash
# helm-diff — show what would change before applying (like terraform plan)
helm plugin install [Link]
# Usage:
helm diff upgrade my-app ./my-chart -f [Link]
# Shows: resources that would be added, changed, or deleted
# ─────────────────────────────────────────────────────────
# helm-secrets — encrypt/decrypt secret values using SOPS
helm plugin install [Link]
# Usage:
# Encrypt secrets file with AWS KMS
sops -e --kms arn:aws:kms:us-east-1:123:key/abc [Link] \
> [Link]
# Deploy with encrypted file — plugin decrypts transparently
helm secrets upgrade --install my-app ./my-chart \
-f [Link] \
-f [Link]
# ─────────────────────────────────────────────────────────
# helm-unittest — unit test your chart templates
helm plugin install [Link]
# Write test in tests/ folder:
# tests/deployment_test.yaml
suite: deployment tests
tests:
- it: should set correct replica count
set:
replicaCount: 3
asserts:
- equal:
path: [Link]
value: 3
documentIndex: 0
# Run tests:
helm unittest ./my-chart
Secrets Management — The Right Way
Option 1 — Sealed Secrets (for GitOps)
bash
# Install Sealed Secrets controller
helm install sealed-secrets \
sealed-secrets/sealed-secrets \
-n kube-system
# Install kubeseal CLI
brew install kubeseal
# Create a sealed secret (safe to commit to git)
kubectl create secret generic my-secret \
--from-literal=db-password=supersecret \
--dry-run=client -o yaml \
| kubeseal --format yaml \
> [Link]
# [Link] is encrypted — safe to commit
# Only the cluster can decrypt it
# Add to chart templates — deploy with Helm
# cp [Link] my-chart/templates/[Link]
Option 2 — External Secrets Operator (recommended)
yaml
# templates/[Link]
apiVersion: [Link]/v1beta1
kind: ExternalSecret
metadata:
name: {{ include "[Link]" . }}-secret
spec:
refreshInterval: 1h # re-sync from source every hour
secretStoreRef:
name: aws-secretsmanager # which store to use
kind: ClusterSecretStore
target:
name: {{ include "[Link]" . }}-secret
creationPolicy: Owner
data:
- secretKey: database-url # key in K8s secret
remoteRef:
key: production/my-app # key in AWS Secrets Manager
property: database_url # field within the secret JSON
- secretKey: api-key
remoteRef:
key: production/my-app
property: api_key
This creates a real Kubernetes Secret by pulling values from AWS Secrets Manager. No secrets in
Helm values. No secrets in Git. The ESO controller handles it.
Option 3 — Pass secrets at deploy time from CI/CD
bash
# Secrets live in GitLab CI/CD variables (masked, protected)
# Passed only at deploy time — never in files
helm upgrade --install my-app ./my-chart \
-f [Link] \
--set [Link]="$PROD_DB_PASSWORD" \
--set [Link]="$API_KEY"
Multi-Environment Strategy — Architect Pattern
environments/
├── base/
│ └── my-chart/ ← the actual Helm chart
│ ├── [Link]
│ ├── [Link] ← shared defaults
│ └── templates/
├── dev/
│ └── [Link] ← dev overrides
├── staging/
│ └── [Link] ← staging overrides
└── production/
├── [Link] ← prod overrides
└── [Link] ← encrypted secrets
bash
# Each environment deploy:
ENV=production
helm upgrade --install my-app ./environments/base/my-chart \
-f ./environments/base/my-chart/[Link] \
-f ./environments/${ENV}/[Link] \
--atomic -n ${ENV}
Security Considerations
bash
# 1. Scan charts for misconfigurations before deploying
helm template my-app ./my-chart -f [Link] \
| checkov -f - # Checkov policy checks
helm template my-app ./my-chart -f [Link] \
| trivy config - # Trivy config scan
helm template my-app ./my-chart -f [Link] \
| kube-score score - # Kubernetes best practices
# 2. Use specific image digests instead of tags in production
# Tags are mutable — digest is immutable
image:
repository: myapp
# tag: "1.5.0" ← mutable, can be overwritten
digest: "sha256:abc123..." ← immutable, always same image
# 3. Set security context in deployment template
securityContext:
runAsNonRoot: true
runAsUser: 10001
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
# 4. Control access to Helm release secrets in K8s
# Helm stores release state as K8s secrets
# Restrict who can read secrets in production namespace
# Use RBAC to limit this
13. Real-World Architecture Example
The System — E-Commerce Platform
Three services: frontend, api, worker. Shared PostgreSQL and Redis. Different environments. CI/CD
via GitLab.
Folder Structure
helm/
├── charts/
│ │
│ ├── frontend/ ← Helm chart for React frontend
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── templates/
│ │ ├── _helpers.tpl
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ │
│ ├── api/ ← Helm chart for [Link] API
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── templates/
│ │ ├── _helpers.tpl
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link] ← PodDisruptionBudget
│ │ └── [Link] ← Helm hook for DB migrations
│ │
│ ├── worker/ ← Helm chart for background worker
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── templates/
│ │ ├── _helpers.tpl
│ │ ├── [Link]
│ │ ├── [Link]
│ │ └── [Link]
│ │
│ └── infrastructure/ ← Shared infra chart (DB, cache)
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── templates/
│ └── (postgresql + redis configured as dependencies)
└── argocd/
├── staging/
│ ├── [Link]
│ ├── [Link]
│ ├── [Link]
│ └── [Link]
└── production/
├── [Link]
├── [Link]
├── [Link]
└── [Link]
infrastructure/[Link]
yaml
apiVersion: v2
name: infrastructure
description: Shared infrastructure (database, cache)
type: application
version: 1.0.0
appVersion: "1.0.0"
dependencies:
- name: postgresql
version: "12.5.6"
repository: [Link]
condition: [Link]
- name: redis
version: "18.1.5"
repository: [Link]
condition: [Link]
infrastructure/[Link]
yaml
postgresql:
enabled: true
auth:
username: ecommerce
password: "" # passed via CI
database: ecommercedb
primary:
persistence:
size: 100Gi
resources:
requests:
cpu: 2000m
memory: 4Gi
readReplicas:
replicaCount: 2 # 2 read replicas for scaling
redis:
enabled: true
auth:
enabled: true
password: "" # passed via CI
master:
persistence:
size: 10Gi
replica:
replicaCount: 1
api/[Link]
yaml
apiVersion: v2
name: api
description: E-Commerce API Service
type: application
version: 2.1.0
appVersion: "3.5.0"
api/[Link]
yaml
replicaCount: 5
image:
repository: [Link]/mycompany/ecommerce-api
tag: "3.5.0"
pullPolicy: Always
service:
type: ClusterIP
port: 80
targetPort: 3000
ingress:
enabled: true
className: nginx
host: [Link]
tls:
enabled: true
secretName: api-tls
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 30
targetCPUUtilizationPercentage: 70
pdb:
enabled: true
minAvailable: 3 # always keep at least 3 pods running
appConfig:
log_level: "warn"
cache_ttl: "300"
db_pool_size: "20"
redis_host: "infrastructure-redis-master"
db_host: "infrastructure-postgresql"
serviceAccount:
create: true
annotations:
[Link]/role-arn: arn:aws:iam::123456789:role/ecommerce-api
# ↑ IRSA — pod gets AWS IAM role without access keys
api/templates/[Link]
yaml
{{- if .[Link] }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "[Link]" . }}
labels:
{{- include "[Link]" . | nindent 4 }}
spec:
{{- if .[Link] }}
minAvailable: {{ .[Link] }}
{{- end }}
selector:
matchLabels:
{{- include "[Link]" . | nindent 6 }}
{{- end }}
ArgoCD Application — api production
yaml
# argocd/production/[Link]
apiVersion: [Link]/v1alpha1
kind: Application
metadata:
name: ecommerce-api-production
namespace: argocd
annotations:
[Link]/[Link]: deployments
[Link]/[Link]: alerts
spec:
project: production
source:
repoURL: [Link]
targetRevision: main
path: helm/charts/api
helm:
valueFiles:
- [Link]
- [Link]
destination:
server: [Link]
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true
retry:
limit: 3
backoff:
duration: 5s
factor: 2
maxDuration: 3m
Architecture Decision Trade-offs
Decision 1: Separate charts per service (not one big chart)
WHY: Each service deploys independently. API team doesn't wait for frontend.
TRADE-OFF: More charts to maintain. Solved by: library chart for common templates.
Decision 2: Separate infrastructure chart for DB/Redis
WHY: DB doesn't deploy on every app release. Separate lifecycle.
TRADE-OFF: Two helm releases to coordinate. Solved by: ArgoCD sync waves.
Decision 3: ArgoCD for GitOps instead of helm upgrade in CI
WHY: Continuous drift correction. No manual kubectl. Full audit trail in Git.
TRADE-OFF: ArgoCD itself needs to be managed. Sync can lag 3 min.
Decision 4: Secrets via CI --set, not in files
WHY: Simple, no extra tooling, secrets never in Git or file system.
TRADE-OFF: Secrets only visible at deploy time. Moved to External Secrets
Operator for rotation support.
14. Hands-On Exercises
Exercise 1 — Create a Chart From Scratch
Goal: Create a Helm chart for a simple [Link] API.
bash
# Step 1: Create chart
helm create nodeapi
# Step 2: Update [Link]
cat > nodeapi/[Link] << EOF
apiVersion: v2
name: nodeapi
description: [Link] API Service
type: application
version: 1.0.0
appVersion: "1.0.0"
EOF
# Step 3: Update [Link]
cat > nodeapi/[Link] << EOF
replicaCount: 1
image:
repository: node
tag: "18-alpine"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 3000
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 200m
memory: 256Mi
env:
NODE_ENV: production
PORT: "3000"
EOF
# Step 4: Lint
helm lint ./nodeapi
# Step 5: Render and inspect
helm template my-nodeapi ./nodeapi
# Step 6: Install
helm install my-nodeapi ./nodeapi
# Step 7: Verify
helm list
kubectl get pods
kubectl get svc
Exercise 2 — Modify Values for Different Environments
bash
# Create staging values
cat > nodeapi/[Link] << EOF
replicaCount: 2
image:
tag: "staging"
env:
NODE_ENV: staging
PORT: "3000"
LOG_LEVEL: debug
EOF
# Create production values
cat > nodeapi/[Link] << EOF
replicaCount: 5
image:
tag: "1.2.0"
resources:
requests:
cpu: 500m
memory: 512Mi
limits:
cpu: 1000m
memory: 1Gi
env:
NODE_ENV: production
PORT: "3000"
LOG_LEVEL: warn
EOF
# Deploy to staging namespace
helm upgrade --install my-nodeapi ./nodeapi \
-f nodeapi/[Link] \
-n staging --create-namespace
# Deploy to production namespace
helm upgrade --install my-nodeapi ./nodeapi \
-f nodeapi/[Link] \
-n production --create-namespace
# Verify both
helm list -n staging
helm list -n production
Exercise 3 — Add a New Service (PostgreSQL dependency)
bash
# Step 1: Add dependency to [Link]
cat >> nodeapi/[Link] << EOF
dependencies:
- name: postgresql
version: "12.1.0"
repository: [Link]
condition: [Link]
EOF
# Step 2: Add postgresql config to [Link]
cat >> nodeapi/[Link] << EOF
postgresql:
enabled: true
auth:
username: nodeapi
password: "devpassword"
database: nodeapidb
primary:
persistence:
size: 1Gi
EOF
# Step 3: Download dependency
helm repo add bitnami [Link]
helm repo update
helm dependency update ./nodeapi
# Step 4: Verify dependency downloaded
ls nodeapi/charts/
# [Link]
# Step 5: Install with DB
helm upgrade --install my-nodeapi ./nodeapi
# Step 6: Verify postgres pod is running
kubectl get pods
# my-nodeapi-postgresql-0 1/1 Running
# my-nodeapi-xxxx 1/1 Running
Exercise 4 — Debug a Failing Deployment
Simulate a failure:
bash
# Deploy with wrong image tag (image doesn't exist)
helm upgrade --install my-nodeapi ./nodeapi \
--set [Link]=nonexistent/image \
--set [Link]=doesnotexist
# Now debug it:
# Step 1: Check release status
helm status my-nodeapi
# Should show FAILED or pods not ready
# Step 2: Check pods
kubectl get pods
# ImagePullBackOff or ErrImagePull
# Step 3: Describe failing pod
kubectl describe pod <pod-name>
# Look at Events section:
# Failed to pull image "nonexistent/image:doesnotexist": ...
# Step 4: Rollback
helm rollback my-nodeapi
# Goes back to last working revision
# Step 5: Verify rollback worked
helm history my-nodeapi
kubectl get pods
# Pods running again
# Step 6: Check history
helm history my-nodeapi
# REVISION STATUS DESCRIPTION
#1 superseded Install complete
#2 failed Upgrade failed
#3 deployed Rollback to 1
Quick Revision Summary
Helm is a package manager for Kubernetes.
Core concepts:
Chart = package (folder with templates + values)
Release = installed instance of a chart
Values = configuration that customises the chart
Revision = version number of a release (increments on every change)
Key files:
[Link] = chart metadata and dependencies
[Link] = default configuration
templates/ = Kubernetes manifests with Go templating
_helpers.tpl = reusable template functions
Key commands:
helm install = first-time install
helm upgrade = update existing release
helm upgrade --install = install or upgrade (use this in CI/CD)
helm rollback = go back to previous revision
helm uninstall = remove everything
helm template = render templates locally
helm lint = check for errors
helm history = see all revisions
helm get values = see current values
Template syntax:
{{ .Values.x }} = access a value
{{ .[Link] }} = release name
{{- if .Values.x }} = conditional
{{- range .Values.x }} = loop
| quote = wrap in quotes
| toYaml | nindent N = convert block + indent
| default "x" = fallback value
Best practices:
Always use --atomic in production
Always pin image tags (never "latest" in prod)
Use --history-max 10 to limit etcd usage
Pass secrets via CI --set, never hardcode
Lint before every deployment
Use environment-specific values files
Use hooks for database migrations