Helm Lab¶
Duration: 25 minutes
Objectives¶
- Create a Helm chart from scratch
- Use templating and values
- Install and upgrade releases
- Work with chart repositories
- Package and distribute charts
Prerequisites¶
- kind cluster running
- Helm 3 installed
- kubectl configured
Lab Tasks¶
Task 1: Verify Helm Installation¶
# Check Helm version
helm version
# Add Bitnami repository
helm repo add bitnami https://charts.bitnami.com/bitnami
# Update repository index
helm repo update
# Search for charts
helm search repo nginx
Expected Output: Helm version 3.x and list of nginx charts from Bitnami.
Solution
helm version
# version.BuildInfo{Version:"v3.x.x", GitCommit:"...", GitTreeState:"clean", GoVersion:"go1.x.x"}
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm search repo nginx
# NAME CHART VERSION APP VERSION DESCRIPTION
# bitnami/nginx 15.x.x 1.25.x NGINX Open Source is a web server...
# bitnami/nginx-ingress-controller 10.x.x 1.9.x NGINX Ingress Controller...
# Get chart information
helm show chart bitnami/nginx
# Get default values
helm show values bitnami/nginx > nginx-defaults.yaml
Task 2: Install a Chart from Repository¶
Install NGINX from the Bitnami repository:
# Create namespace
kubectl create namespace helm-demo
# Install NGINX chart
helm install my-nginx bitnami/nginx \
--namespace helm-demo \
--set service.type=NodePort
# List releases
helm list -n helm-demo
# Check deployed resources
kubectl get all -n helm-demo
# Get release status
helm status my-nginx -n helm-demo
Expected Output: NGINX deployment, service, and pods running in helm-demo namespace.
Solution
helm install my-nginx bitnami/nginx --set replicaCount=2
# Output:
# NAME: my-nginx
# LAST DEPLOYED: ...
# NAMESPACE: default
# STATUS: deployed
# REVISION: 1
kubectl get pods -l app.kubernetes.io/instance=my-nginx
# NAME READY STATUS RESTARTS AGE
# my-nginx-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
# my-nginx-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
kubectl get svc my-nginx
# Test the nginx deployment
kubectl port-forward svc/my-nginx 8080:80
# Visit http://localhost:8080 - should see nginx welcome page
Task 3: Customize with Values¶
Create a custom values file to override defaults:
Create custom-values.yaml:
replicaCount: 3
service:
type: NodePort
nodePorts:
http: 30080
resources:
limits:
cpu: 300m
memory: 384Mi
requests:
cpu: 150m
memory: 192Mi
Upgrade the release with custom values:
# Upgrade with custom values
helm upgrade my-nginx bitnami/nginx \
--namespace helm-demo \
-f custom-values.yaml
# Verify changes
kubectl get pods -n helm-demo
kubectl get svc -n helm-demo
# Check revision history
helm history my-nginx -n helm-demo
Test the service:
Solution
Task 4: Create Your Own Chart¶
Create a custom Helm chart for a simple application:
# Create chart scaffold
helm create myapp
# Explore the structure
tree myapp
# Chart structure:
# myapp/
# ├── Chart.yaml
# ├── values.yaml
# ├── charts/
# └── templates/
# ├── deployment.yaml
# ├── service.yaml
# ├── ingress.yaml
# └── ...
Customize the chart:
Edit myapp/values.yaml:
replicaCount: 2
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.25-alpine"
service:
type: ClusterIP
port: 8080
ingress:
enabled: false
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Solution
helm create myapp
# This creates:
# myapp/
# Chart.yaml
# values.yaml
# templates/
# deployment.yaml
# service.yaml
# ingress.yaml
# ...
Modify myapp/values.yaml:
replicaCount: 2
image:
repository: nginx
pullPolicy: IfNotPresent
tag: "1.25.3"
service:
type: ClusterIP
port: 80
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
Install the chart:
Task 5: Template and Install Your Chart¶
Preview the rendered templates:
# Render templates (dry run)
helm template myapp ./myapp
# Validate the chart
helm lint ./myapp
# Install with dry-run to see what would be created
helm install test-release ./myapp --dry-run --debug
Install the chart:
# Install the chart
helm install myapp-release ./myapp --namespace helm-demo
# Verify installation
helm list -n helm-demo
kubectl get all -n helm-demo -l app.kubernetes.io/instance=myapp-release
# Get the rendered manifest
helm get manifest myapp-release -n helm-demo
Solution
Add environment variable to myapp/templates/deployment.yaml:
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
# Add this section:
env:
- name: APP_ENV
value: {{ .Values.environment | default "development" | quote }}
- name: APP_VERSION
value: {{ .Chart.Version | quote }}
Add to myapp/values.yaml:
Test template rendering:
Task 6: Override Values¶
Install another release with different values:
# Install with overrides
helm install myapp-prod ./myapp \
--namespace helm-demo \
--set replicaCount=5 \
--set image.tag=1.26-alpine \
--set service.port=9090
# List both releases
helm list -n helm-demo
# Compare pods
kubectl get pods -n helm-demo -l app.kubernetes.io/name=myapp
Solution
helm upgrade myapp ./myapp --set replicaCount=3
# Output:
# Release "myapp" has been upgraded. Happy Helming!
# NAME: myapp
# LAST DEPLOYED: ...
# STATUS: deployed
# REVISION: 2
kubectl get pods -l app.kubernetes.io/name=myapp
# Output shows 3 pods
helm history myapp
# REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
# 1 Mon Jan 1 10:00:00 2024 superseded myapp-0.1.0 1.16.0 Install complete
# 2 Mon Jan 1 10:05:00 2024 deployed myapp-0.1.0 1.16.0 Upgrade complete
Task 7: Upgrade and Rollback¶
Upgrade the release:
# Upgrade with new values
helm upgrade myapp-release ./myapp \
--namespace helm-demo \
--set replicaCount=4
# View history
helm history myapp-release -n helm-demo
# Check the change
kubectl get deployment -n helm-demo
Rollback if needed:
# Rollback to previous version
helm rollback myapp-release -n helm-demo
# Verify rollback
helm history myapp-release -n helm-demo
kubectl get deployment -n helm-demo
Solution
helm rollback myapp 1
# Output:
# Rollback was a success! Happy Helming!
kubectl get pods -l app.kubernetes.io/name=myapp
# Output shows 2 pods again
helm history myapp
# REVISION UPDATED STATUS CHART APP VERSION DESCRIPTION
# 1 Mon Jan 1 10:00:00 2024 superseded myapp-0.1.0 1.16.0 Install complete
# 2 Mon Jan 1 10:05:00 2024 superseded myapp-0.1.0 1.16.0 Upgrade complete
# 3 Mon Jan 1 10:10:00 2024 deployed myapp-0.1.0 1.16.0 Rollback to 1
Task 8: Add Conditional Resources¶
Modify your chart to conditionally create an Ingress.
Edit myapp/templates/ingress.yaml (if not exists, create it):
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ include "myapp.fullname" . }}
port:
number: {{ .Values.service.port }}
{{- end }}
Update myapp/values.yaml:
Test with Ingress enabled:
# Upgrade with Ingress enabled
helm upgrade myapp-release ./myapp \
--namespace helm-demo \
--set ingress.enabled=true \
--set ingress.host=myapp.local
# Verify Ingress created
kubectl get ingress -n helm-demo
Solution
Create myapp/templates/configmap.yaml:
{{- if .Values.config.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "myapp.fullname" . }}-config
labels:
{{- include "myapp.labels" . | nindent 4 }}
data:
{{- range $key, $value := .Values.config.data }}
{{ $key }}: {{ $value | quote }}
{{- end }}
{{- end }}
Add to myapp/values.yaml:
Mount ConfigMap in myapp/templates/deployment.yaml:
spec:
template:
spec:
containers:
- name: {{ .Chart.Name }}
# ... existing config ...
{{- if .Values.config.enabled }}
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
{{- end }}
Test:
Task 9: Package and Share¶
Package your chart for distribution:
# Package the chart
helm package ./myapp
# This creates: myapp-0.1.0.tgz
# Install from package
helm install myapp-packaged ./myapp-0.1.0.tgz --namespace helm-demo
# List all releases
helm list -n helm-demo
Solution
Task 10: Work with Chart Dependencies¶
Add a dependency to your chart.
Edit myapp/Chart.yaml:
apiVersion: v2
name: myapp
description: A Helm chart for Kubernetes
type: application
version: 0.1.0
appVersion: "1.16.0"
dependencies:
- name: redis
version: "17.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
Update myapp/values.yaml:
Download dependencies and install:
# Update dependencies
helm dependency update ./myapp
# This downloads charts to myapp/charts/
# List dependencies
helm dependency list ./myapp
# Install with dependency
helm install myapp-with-redis ./myapp \
--namespace helm-demo \
--set redis.enabled=true
# Verify Redis is deployed
kubectl get pods -n helm-demo | grep redis
Solution
Create new chart:
Modify webapp/Chart.yaml to add dependency:
apiVersion: v2
name: webapp
description: A Helm chart for webapp with Redis
type: application
version: 0.1.0
appVersion: "1.0"
dependencies:
- name: redis
version: "18.x.x"
repository: https://charts.bitnami.com/bitnami
condition: redis.enabled
tags:
- cache
Modify webapp/values.yaml:
replicaCount: 2
image:
repository: nginx
tag: "1.25.3"
redis:
enabled: true
auth:
enabled: false
master:
persistence:
enabled: false
Update and install:
helm dependency update webapp/
# Output:
# Hang tight while we grab the latest from your chart repositories...
# ...Successfully got an update from the "bitnami" chart repository
# Saving 1 charts
# Downloading redis from repo https://charts.bitnami.com/bitnami
ls -la webapp/charts/
# redis-18.x.x.tgz
helm install webapp ./webapp
kubectl get pods
# NAME READY STATUS RESTARTS AGE
# webapp-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
# webapp-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
# webapp-redis-master-0 1/1 Running 0 30s
# Test disabling Redis
helm upgrade webapp ./webapp --set redis.enabled=false
# Verify only webapp pods remain
kubectl get pods
Verification¶
Check all your releases:
# List all releases in namespace
helm list -n helm-demo
# Get details of each release
helm status my-nginx -n helm-demo
helm status myapp-release -n helm-demo
# View all resources
kubectl get all -n helm-demo
Cleanup¶
# Uninstall all releases
helm uninstall my-nginx -n helm-demo
helm uninstall myapp-release -n helm-demo
helm uninstall myapp-prod -n helm-demo
helm uninstall myapp-packaged -n helm-demo
helm uninstall myapp-with-redis -n helm-demo
# Delete namespace
kubectl delete namespace helm-demo
# Remove repository (optional)
helm repo remove bitnami
Bonus Challenges¶
Challenge 1: Multi-Environment Values¶
Create separate values files for dev, staging, and prod environments.
Create values-dev.yaml:
Create values-prod.yaml:
Install for different environments:
helm install myapp-dev ./myapp -f values-dev.yaml
helm install myapp-prod ./myapp -f values-prod.yaml
Solution
Create values-dev.yaml:
replicaCount: 1
environment: development
resources:
limits:
cpu: 50m
memory: 64Mi
requests:
cpu: 25m
memory: 32Mi
Create values-prod.yaml:
replicaCount: 3
environment: production
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Install with different values:
Challenge 2: Add Health Checks¶
Modify myapp/templates/deployment.yaml to include liveness and readiness probes using values.
Add to values.yaml:
probes:
liveness:
httpGet:
path: /
port: http
initialDelaySeconds: 30
periodSeconds: 10
readiness:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 5
Update deployment template to use these values.
Solution
Create myapp/templates/_helpers.tpl:
{{/*
Create a default fully qualified app name with environment
*/}}
{{- define "myapp.fullname.env" -}}
{{- include "myapp.fullname" . }}-{{ .Values.environment | default "dev" }}
{{- end }}
{{/*
Common labels with custom additions
*/}}
{{- define "myapp.customLabels" -}}
{{ include "myapp.labels" . }}
environment: {{ .Values.environment | default "development" }}
version: {{ .Chart.Version }}
{{- end }}
{{/*
Resource limits and requests template
*/}}
{{- define "myapp.resources" -}}
resources:
limits:
cpu: {{ .Values.resources.limits.cpu | default "100m" }}
memory: {{ .Values.resources.limits.memory | default "128Mi" }}
requests:
cpu: {{ .Values.resources.requests.cpu | default "50m" }}
memory: {{ .Values.resources.requests.memory | default "64Mi" }}
{{- end }}
Use in myapp/templates/deployment.yaml:
Challenge 3: Create a Helper Function¶
Add a helper function to myapp/templates/_helpers.tpl that generates resource names with environment prefix.
{{/*
Generate name with environment
*/}}
{{- define "myapp.envName" -}}
{{- if .Values.environment }}
{{- printf "%s-%s" .Values.environment (include "myapp.fullname" .) }}
{{- else }}
{{- include "myapp.fullname" . }}
{{- end }}
{{- end }}
Use it in templates:
Challenge 4: Chart Hooks¶
Add a pre-install hook that runs a job before the application deploys.
Create myapp/templates/pre-install-job.yaml:
apiVersion: batch/v1
kind: Job
metadata:
name: {{ include "myapp.fullname" . }}-preinstall
annotations:
"helm.sh/hook": pre-install
"helm.sh/hook-weight": "0"
"helm.sh/hook-delete-policy": hook-succeeded
spec:
template:
spec:
containers:
- name: pre-install
image: busybox
command: ['sh', '-c', 'echo "Running pre-install checks..."; sleep 5; echo "Done!"']
restartPolicy: Never
Solution
Create myapp/templates/tests/test-connection.yaml:
apiVersion: v1
kind: Pod
metadata:
name: "{{ include "myapp.fullname" . }}-test-connection"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": test
"helm.sh/hook-delete-policy": hook-succeeded
spec:
containers:
- name: wget
image: busybox
command: ['wget']
args: ['{{ include "myapp.fullname" . }}:{{ .Values.service.port }}']
restartPolicy: Never
Run test:
helm install myapp ./myapp
helm test myapp
# Output:
# NAME: myapp
# ...
# TEST SUITE: myapp-test-connection
# Last Started: Mon Jan 1 10:00:00 2024
# Last Completed: Mon Jan 1 10:00:05 2024
# Phase: Succeeded
Hook example (myapp/templates/job-init.yaml):
apiVersion: batch/v1
kind: Job
metadata:
name: "{{ include "myapp.fullname" . }}-init"
labels:
{{- include "myapp.labels" . | nindent 4 }}
annotations:
"helm.sh/hook": pre-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation
spec:
template:
metadata:
name: "{{ include "myapp.fullname" . }}-init"
spec:
restartPolicy: Never
containers:
- name: init
image: busybox
command: ['sh', '-c', 'echo "Performing initialization..."; sleep 5; echo "Done!"']
Troubleshooting Tips¶
Chart validation fails:
- Run
helm lint ./myappto see specific errors - Check YAML syntax and indentation
- Verify template syntax with
helm template
Release fails to install:
- Check logs:
kubectl logs <pod-name> - Describe resources:
kubectl describe pod <pod-name> - Review release:
helm status <release-name>
Values not applied:
- Verify precedence: defaults < values file < --set
- Check spelling of value keys
- Use
helm get values <release-name>to see applied values
Template errors:
- Use
--dry-run --debugto see rendered templates - Check required values with
{{ required "message" .Values.key }}
Summary¶
You've learned to:
- Install charts from repositories
- Create custom Helm charts from scratch
- Use templating and values for customization
- Install, upgrade, and rollback releases
- Work with multiple releases
- Add conditional resources
- Package and distribute charts
- Manage chart dependencies
- Use Helm hooks for lifecycle management
Helm provides powerful package management for Kubernetes, making it easier to deploy, version, and manage complex applications.
Key takeaways¶
- Helm charts package Kubernetes manifests with templating and versioning into a single deployable unit
- Values files separate configuration from chart structure — customize without modifying templates
helm upgrade --installis idempotent — safe to run repeatedly in CI/CD pipelines- Rollbacks restore a previous release state in seconds with
helm rollback - Chart dependencies allow composing complex applications from reusable sub-charts
- Hooks enable lifecycle actions such as database migrations before upgrades and cleanups after deletion
Next section¶
Once you've reviewed the content and completed the lab, proceed to the next section.