Kubernetes manifests can become hundreds of lines across dozens of files. Helm is the package manager that tames this complexity. Charts package your applications. Values customize deployments for different environments. Releases track deployments over time. In this lesson, you'll learn to create, package, and deploy Helm charts like a pro.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Install and configure Helm
  • Create Helm charts from scratch
  • Use templates with Go templating
  • Manage values for different environments
  • Package and share charts
  • Deploy and upgrade releases
  • Use Helm repositories

2. Why This Matters

Real-world scenario: Your microservices app has 20 Kubernetes YAML files. Deploying to dev, staging, and production requires changing values in multiple files. Helm templatizes your manifests, lets you define values per environment, and deploys everything with one command.

3. Core Concepts

Helm Installation

# Install Helm (Linux)
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

# macOS
brew install helm

# Windows (choco)
choco install kubernetes-helm

# Verify installation
helm version
helm help

# Add repositories
helm repo add stable https://charts.helm.sh/stable
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update

# Search for charts
helm search hub nginx
helm search repo nginx

# Install a chart
helm install my-nginx ingress-nginx/ingress-nginx

# List releases
helm list
helm list --all-namespaces

# Uninstall
helm uninstall my-nginx
Helm installation and basic commands

Chart Structure

# Create a new chart
helm create myapp

# Chart structure:
myapp/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default values
├── values-dev.yaml     # Dev environment values (custom)
├── values-prod.yaml    # Prod environment values (custom)
├── charts/             # Dependencies
├── templates/          # Kubernetes manifests
│   ├── NOTES.txt       # Help text after install
│   ├── _helpers.tpl    # Template helpers
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── configmap.yaml
│   └── tests/          # Test manifests
└── .helmignore         # Files to ignore

# Examine chart contents
helm show chart myapp/
helm show values myapp/
Helm chart structure

Chart.yaml - Metadata

# Chart.yaml
apiVersion: v2
name: myapp
description: A Helm chart for my Kubernetes application
type: application
version: 0.1.0
appVersion: "1.16.0"

# Dependencies
dependencies:
- name: postgresql
  version: 12.x.x
  repository: https://charts.bitnami.com/bitnami
  condition: postgresql.enabled
- name: redis
  version: 17.x.x
  repository: https://charts.bitnami.com/bitnami
  condition: redis.enabled

# Maintainers
maintainers:
- name: Your Name
  email: your.email@example.com

# Keywords for search
keywords:
- myapp
- web
- backend

# Homepage
home: https://example.com

# Sources
sources:
- https://github.com/example/myapp

# Annotations (for Helm charts repository)
annotations:
  artifacthub.io/license: MIT
  artifacthub.io/operator: "false"
Chart.yaml configuration

Values.yaml - Default Configuration

# values.yaml
# Global settings
global:
  environment: production
  imagePullSecrets: []

# Replica count
replicaCount: 2

# Image configuration
image:
  repository: myapp
  pullPolicy: IfNotPresent
  tag: latest

# Image pull secrets
imagePullSecrets: []

# Name override
nameOverride: ""
fullnameOverride: ""

# ServiceAccount
serviceAccount:
  create: true
  annotations: {}
  name: ""

# Pod annotations
podAnnotations: {}

# Pod security context
podSecurityContext: {}
  # fsGroup: 2000

# Container security context
securityContext: {}
  # capabilities:
  #   drop:
  #   - ALL
  # readOnlyRootFilesystem: true
  # runAsNonRoot: true
  # runAsUser: 1000

# Service configuration
service:
  type: ClusterIP
  port: 80
  targetPort: 8080

# Ingress configuration
ingress:
  enabled: false
  className: ""
  annotations: {}
  hosts:
  - host: chart-example.local
    paths:
    - path: /
      pathType: ImplementationSpecific
  tls: []

# Resource limits
resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

# Autoscaling
autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

# Node selector
nodeSelector: {}

# Tolerations
tolerations: []

# Affinity
affinity: {}

# Environment variables (custom to your app)
env:
  LOG_LEVEL: info
  API_URL: https://api.example.com

# ConfigMap data
configMap:
  enabled: true
  data:
    app.properties: |
      app.name=myapp
      app.version=1.0

# Secret data (base64 encoded)
secret:
  enabled: true
  data:
    api-key: ""  # Will be set via --set or external secrets
values.yaml - Default configuration

Templates with Go Templating

# templates/_helpers.tpl - Helper functions
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: {{ .Values.service.targetPort }}
          name: http
        env:
        {{- range $key, $value := .Values.env }}
        - name: {{ $key }}
          value: {{ $value | quote }}
        {{- end }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
      {{- with .Values.nodeSelector }}
      nodeSelector:
        {{- toYaml . | nindent 8 }}
      {{- end }}
Helm templates with Go templating

Environment Values

# values-dev.yaml
replicaCount: 1

image:
  tag: dev-latest

service:
  type: ClusterIP

ingress:
  enabled: false

env:
  LOG_LEVEL: debug
  API_URL: https://dev-api.example.com

resources:
  limits:
    cpu: 200m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

autoscaling:
  enabled: false

# values-staging.yaml
replicaCount: 2

image:
  tag: staging-latest

service:
  type: ClusterIP

ingress:
  enabled: true
  host: staging.example.com

env:
  LOG_LEVEL: info
  API_URL: https://staging-api.example.com

resources:
  limits:
    cpu: 500m
    memory: 512Mi
  requests:
    cpu: 250m
    memory: 256Mi

# values-prod.yaml
replicaCount: 5

image:
  tag: latest

service:
  type: LoadBalancer

ingress:
  enabled: true
  host: example.com
  tls:
  - hosts:
    - example.com
    secretName: tls-secret

env:
  LOG_LEVEL: warn
  API_URL: https://api.example.com

resources:
  limits:
    cpu: 1000m
    memory: 1Gi
  requests:
    cpu: 500m
    memory: 512Mi

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
Environment-specific values files

4. Complete Project: Custom Helm Chart

# Create chart
helm create myapp-chart
cd myapp-chart

# Customize templates
# Edit templates/deployment.yaml, service.yaml, etc.

# Create environment values
cat > values-dev.yaml << 'EOF'
environment: dev
replicaCount: 1
ingress:
  enabled: false
EOF

cat > values-staging.yaml << 'EOF'
environment: staging
replicaCount: 2
ingress:
  enabled: true
  host: staging.myapp.com
EOF

cat > values-prod.yaml << 'EOF'
environment: production
replicaCount: 5
ingress:
  enabled: true
  host: myapp.com
  tls:
  - secretName: myapp-tls
    hosts:
    - myapp.com
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
EOF

# Lint chart
helm lint .

# Template rendering (dry-run)
helm template myapp .

# Install with different environments
helm install myapp-dev . -f values-dev.yaml --namespace dev --create-namespace
helm install myapp-staging . -f values-staging.yaml --namespace staging --create-namespace
helm install myapp-prod . -f values-prod.yaml --namespace prod --create-namespace

# Override values on command line
helm install myapp . --set replicaCount=3 --set image.tag=v1.2.3

# Upgrade release
helm upgrade myapp-staging . -f values-staging.yaml --set image.tag=v1.2.4

# Rollback
helm rollback myapp-prod 2

# List releases
helm list -A

# Get release manifest
helm get manifest myapp-prod

# Get release values
helm get values myapp-prod

# Uninstall
helm uninstall myapp-dev -n dev
Complete Helm chart workflow

Helm Chart Dependencies

# Chart.yaml with dependencies
apiVersion: v2
name: myapp-with-deps
dependencies:
- name: postgresql
  version: "12.x.x"
  repository: "https://charts.bitnami.com/bitnami"
  condition: postgresql.enabled
- name: redis
  version: "17.x.x"
  repository: "https://charts.bitnami.com/bitnami"
  condition: redis.enabled
- name: prometheus
  version: "23.x.x"
  repository: "https://prometheus-community.github.io/helm-charts"
  condition: monitoring.enabled
- name: grafana
  version: "6.x.x"
  repository: "https://grafana.github.io/helm-charts"
  condition: monitoring.enabled

# Update dependencies
helm dependency update

# List dependencies
helm dependency list

# Build dependencies directory
helm dependency build

# values.yaml for dependencies
postgresql:
  enabled: true
  auth:
    postgresPassword: "secret"
    database: myapp
  primary:
    persistence:
      size: 10Gi

redis:
  enabled: true
  architecture: standalone
  auth:
    password: "redis-secret"

monitoring:
  enabled: false

prometheus:
  server:
    persistentVolume:
      size: 50Gi

grafana:
  adminPassword: admin
Managing chart dependencies

Packaging and Sharing

# Package chart
helm package myapp-chart
# Creates: myapp-chart-0.1.0.tgz

# Push to repository (OCI registry)
helm registry login registry.example.com
helm push myapp-chart-0.1.0.tgz oci://registry.example.com/helm

# Or use ChartMuseum (open-source Helm repo)
docker run -d -p 8080:8080 -e STORAGE=local -e STORAGE_LOCAL_ROOT=/charts chartmuseum/chartmuseum

# Push to ChartMuseum
curl --data-binary "@myapp-chart-0.1.0.tgz" http://localhost:8080/api/charts

# Add repository
helm repo add myrepo http://localhost:8080
helm repo update

# Install from repository
helm install myapp myrepo/myapp-chart

# Create GitHub Pages Helm repo
# 1. Create gh-pages branch
# 2. Place charts in repository
# 3. Generate index.yaml
helm repo index . --url https://username.github.io/repo

# Then add
helm repo add myrepo https://username.github.io/repo
Packaging and sharing Helm charts

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next category: CI/CD - Advanced Pipelines (Lessons 7.2-7.5)

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 13, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment