0% found this document useful (0 votes)
2 views92 pages

Helm Complete Tutorial

The document is a comprehensive tutorial on Helm, a package manager for Kubernetes, detailing its purpose, installation steps, and command usage. It addresses common problems Helm solves, such as managing multiple YAML files, environment configurations, rollback mechanisms, and release tracking. Additionally, it explains the structure of Helm charts and the significance of various files within a chart.

Uploaded by

Dheeraj Dubey
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views92 pages

Helm Complete Tutorial

The document is a comprehensive tutorial on Helm, a package manager for Kubernetes, detailing its purpose, installation steps, and command usage. It addresses common problems Helm solves, such as managing multiple YAML files, environment configurations, rollback mechanisms, and release tracking. Additionally, it explains the structure of Helm charts and the significance of various files within a chart.

Uploaded by

Dheeraj Dubey
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd

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

You might also like