Ever felt like you’re drowning in a sea of Kubernetes YAML files? Copy-pasting configurations across environments, hunting down that one value that’s different between dev and prod, praying you didn’t introduce a typo that will crash your production deployment at 3 AM?
You’re not alone. Managing Kubernetes manifests manually is like trying to conduct an orchestra where every instrument plays from a different sheet of music. It’s chaotic, error-prone, and frankly, there’s a better way.
Enter Helmβthe package manager and template engine that transforms Kubernetes chaos into orchestrated elegance. By the end of this guide, you’ll understand how to package, template, and deploy applications across environments with the confidence of a seasoned platform engineer.
Let’s dive in.
Table of Contents
- Understanding Helm Charts: The Building Blocks
- Hands-On: Building Your First Chart
- Multi-Environment Mastery
- From Templates to Production: The GitOps Connection
- Real-World Example: Complete Application Chart
<a name=”understanding-helm-charts”></a>
1. Understanding Helm Charts: The Building Blocks
A Helm chart is a collection of files that describes a complete Kubernetes application. Think of it as a blueprint that can be customized for different scenarios without rewriting the entire structure.
The Three Pillars of Every Chart
Every Helm chart, from the simplest to the most complex, is built on three essential components:
π Chart.yaml – The chart’s identity and metadata
apiVersion: v2
name: my-hello-chart
description: A basic Hello World Helm chart
version: 0.1.0
appVersion: "1.0.0"
keywords:
- example
- tutorial
maintainers:
- name: Your Name
email: your.email@example.com
π templates/ – The templated Kubernetes manifests This directory contains your Kubernetes YAML files, but with special placeholders that Helm will replace with actual values during deployment.
βοΈ values.yaml – The configuration hub
# Default configuration values for the chart
# These can be overridden during installation
# Application configuration
replicaCount: 1
image:
repository: nginx
tag: "1.21.0"
pullPolicy: IfNotPresent
# Service configuration
service:
type: ClusterIP
port: 80
# Resource limits
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
π‘ Tip: Always document your
values.yamlwith comments. Your future self (and teammates) will thank you when they need to customize deployments six months from now.
<a name=”building-your-first-chart”></a>
2. Hands-On: Building Your First Chart
Let’s build a real chart from scratch. We’ll create a simple web application that demonstrates the core concepts.
Chart Structure
First, create this directory structure:
my-webapp-chart/
βββ Chart.yaml
βββ values.yaml
βββ templates/
β βββ deployment.yaml
β βββ service.yaml
β βββ configmap.yaml
βββ .helmignore
Step 1: Define Chart Metadata
Create Chart.yaml:
apiVersion: v2
name: my-webapp-chart
description: A production-ready web application Helm chart
type: application
version: 1.0.0
appVersion: "2.0.1"
Step 2: Create the Deployment Template
Create templates/deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-webapp
labels:
app: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
environment: {{ .Values.environment }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
app: {{ .Release.Name }}
template:
metadata:
labels:
app: {{ .Release.Name }}
environment: {{ .Values.environment }}
spec:
containers:
- name: webapp
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
protocol: TCP
env:
- name: APP_MESSAGE
valueFrom:
configMapKeyRef:
name: {{ .Release.Name }}-config
key: message
- name: ENVIRONMENT
value: {{ .Values.environment }}
resources:
{{- toYaml .Values.resources | nindent 10 }}
livenessProbe:
httpGet:
path: /health
port: http
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: http
initialDelaySeconds: 5
periodSeconds: 5
Step 3: Create the Service Template
Create templates/service.yaml:
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-webapp-service
labels:
app: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
spec:
type: {{ .Values.service.type }}
ports:
- port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
name: http
selector:
app: {{ .Release.Name }}
Step 4: Create the ConfigMap Template
Create templates/configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Release.Name }}-config
labels:
app: {{ .Release.Name }}
chart: {{ .Chart.Name }}-{{ .Chart.Version }}
data:
message: {{ .Values.config.message | quote }}
log_level: {{ .Values.config.logLevel | quote }}
Step 5: Define Default Values
Create values.yaml:
# Default values for my-webapp-chart
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.
# Environment identifier
environment: dev
# Number of pod replicas
replicaCount: 1
# Container image configuration
image:
repository: nginx
tag: "1.21.0"
pullPolicy: IfNotPresent
# Service configuration
service:
type: ClusterIP
port: 80
targetPort: 8080
# Application configuration
config:
message: "Hello from Development!"
logLevel: "debug"
# Resource limits and requests
resources:
limits:
cpu: 200m
memory: 256Mi
requests:
cpu: 100m
memory: 128Mi
Step 6: Test Your Chart
Render the templates locally to see the final YAML:
# From the chart directory
helm template my-webapp .
You should see fully rendered Kubernetes manifests with all placeholders replaced by values from values.yaml.
<a name=”multi-environment-mastery”></a>
3. Multi-Environment Mastery: The Power of Value Overrides
Here’s where Helm truly shines. Instead of maintaining separate manifest files for each environment, you create environment-specific value files that override only what’s different.
Create Environment-Specific Values
values-dev.yaml (Development):
environment: dev
replicaCount: 1
image:
tag: "latest"
pullPolicy: Always
config:
message: "Hello from Development! π οΈ"
logLevel: "debug"
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
values-staging.yaml (Staging):
environment: staging
replicaCount: 2
image:
tag: "v1.2.3"
pullPolicy: IfNotPresent
config:
message: "Hello from Staging! π§ͺ"
logLevel: "info"
service:
type: LoadBalancer
resources:
limits:
cpu: 300m
memory: 512Mi
requests:
cpu: 150m
memory: 256Mi
values-prod.yaml (Production):
environment: production
replicaCount: 5
image:
tag: "v1.2.3"
pullPolicy: IfNotPresent
config:
message: "Hello from Production! π"
logLevel: "error"
service:
type: LoadBalancer
port: 443
resources:
limits:
cpu: 500m
memory: 1Gi
requests:
cpu: 250m
memory: 512Mi
Deploy to Different Environments
# Development
helm template my-webapp . -f values-dev.yaml
# Staging
helm template my-webapp . -f values-staging.yaml
# Production
helm template my-webapp . -f values-prod.yaml
Compare the Results
When you run the production template, notice how the deployment spec changes:
# Generated from values-prod.yaml
spec:
replicas: 5 # Was 1 in dev
template:
spec:
containers:
- name: webapp
image: "nginx:v1.2.3" # Specific version, not 'latest'
env:
- name: APP_MESSAGE
value: "Hello from Production! π"
- name: ENVIRONMENT
value: production
resources:
limits:
cpu: 500m # Scaled up for production
memory: 1Gi
This is the magic: One set of templates, multiple environments, zero configuration drift.
<a name=”gitops-connection”></a>
4. From Templates to Production: The GitOps Connection
While helm template is great for testing, production deployments use Helm as part of a GitOps workflow. This creates a fully automated, auditable deployment pipeline.
How GitOps Works with Helm
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Git Repo β βββ> β ArgoCD/ β βββ> β Kubernetes β
β (Source) β β Flux CD β β Cluster β
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β β β
β Helm Charts + β Detects Changes β Applies
β Values Files β Runs helm template β Manifests
βββββββββββββββββββββββββ΄ββββββββββββββββββββββ
Single Source of Truth
Example GitOps Repository Structure
my-app-gitops/
βββ charts/
β βββ my-webapp-chart/
β βββ Chart.yaml
β βββ values.yaml
β βββ templates/
β βββ deployment.yaml
β βββ service.yaml
β βββ configmap.yaml
βββ environments/
βββ dev/
β βββ values.yaml
βββ staging/
β βββ values.yaml
βββ prod/
βββ values.yaml
ArgoCD Application Manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-webapp-prod
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/yourorg/my-app-gitops
targetRevision: main
path: charts/my-webapp-chart
helm:
valueFiles:
- ../../environments/prod/values.yaml
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
The workflow:
- Developer commits changes to
environments/prod/values.yaml - ArgoCD detects the change
- ArgoCD runs
helm templatewith the new values - ArgoCD applies the rendered manifests to the cluster
- Your application updates automatically
π― Expert Insight: In mature GitOps workflows, humans rarely run
helm installorhelm upgrademanually. Git commits become deployments, creating a complete audit trail of who changed what, when, and why.
<a name=”real-world-example”></a>
5. Real-World Example: Complete Application Chart
Let’s put it all together with a realistic microservice application that includes advanced Helm features.
Advanced Chart Structure
microservice-chart/
βββ Chart.yaml
βββ values.yaml
βββ templates/
β βββ _helpers.tpl
β βββ deployment.yaml
β βββ service.yaml
β βββ ingress.yaml
β βββ configmap.yaml
β βββ secret.yaml
β βββ hpa.yaml
β βββ serviceaccount.yaml
βββ values/
βββ dev.yaml
βββ staging.yaml
βββ prod.yaml
Helper Template (templates/_helpers.tpl)
{{/*
Expand the name of the chart.
*/}}
{{- define "microservice.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "microservice.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "microservice.labels" -}}
helm.sh/chart: {{ include "microservice.chart" . }}
{{ include "microservice.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "microservice.selectorLabels" -}}
app.kubernetes.io/name: {{ include "microservice.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Horizontal Pod Autoscaler (templates/hpa.yaml)
{{- if .Values.autoscaling.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ include "microservice.fullname" . }}
labels:
{{- include "microservice.labels" . | nindent 4 }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ include "microservice.fullname" . }}
minReplicas: {{ .Values.autoscaling.minReplicas }}
maxReplicas: {{ .Values.autoscaling.maxReplicas }}
metrics:
{{- if .Values.autoscaling.targetCPUUtilizationPercentage }}
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
{{- end }}
{{- if .Values.autoscaling.targetMemoryUtilizationPercentage }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }}
{{- end }}
{{- end }}
Production Values (values/prod.yaml)
# Production environment configuration
environment: production
replicaCount: 3
image:
repository: mycompany/microservice
tag: "v2.1.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/rate-limit: "100"
hosts:
- host: api.mycompany.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: api-tls
hosts:
- api.mycompany.com
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
targetMemoryUtilizationPercentage: 80
resources:
limits:
cpu: 1000m
memory: 2Gi
requests:
cpu: 500m
memory: 1Gi
config:
logLevel: "warn"
database:
host: postgres.production.svc.cluster.local
port: 5432
name: production_db
redis:
host: redis.production.svc.cluster.local
port: 6379
secrets:
database:
username: prod_user
# Password stored in external secret manager
apiKeys:
externalService: "vault:secret/prod/api-keys#external-service"
Conclusion: Your Path to Kubernetes Mastery
You’ve journeyed from YAML chaos to Helm mastery. You now understand:
β
How Helm charts are structured and why
β
How to create reusable templates with dynamic values
β
How to manage multiple environments without duplication
β
How Helm integrates into modern GitOps workflows
β
Real-world patterns for production deployments
The transformation: What once required maintaining dozens of similar YAML files now requires one chart and a handful of value files. What once meant manual deployments and configuration drift now means automated, auditable GitOps deployments.
Next Steps
- Practice: Convert one of your existing applications to a Helm chart
- Explore: Check out Artifact Hub for community charts
- Level Up: Learn about chart dependencies, hooks, and testing with
helm test - Go GitOps: Implement ArgoCD or Flux CD in your cluster
Remember: Every Kubernetes expert started exactly where you are now. The difference? They took the first step.
What will you package and deploy first?
Like this article, comment your thoughts on what are the other topics you like us to write about
Found this guide helpful? Share it with your team and let’s make Kubernetes management better for everyone.