Cloud Computing Kubernetes Orchestration

Helm Charts Unlocked: Your Ultimate Guide from Zero to Kubernetes Hero!

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

  1. Understanding Helm Charts: The Building Blocks
  2. Hands-On: Building Your First Chart
  3. Multi-Environment Mastery
  4. From Templates to Production: The GitOps Connection
  5. 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.yaml with 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:

  1. Developer commits changes to environments/prod/values.yaml
  2. ArgoCD detects the change
  3. ArgoCD runs helm template with the new values
  4. ArgoCD applies the rendered manifests to the cluster
  5. Your application updates automatically

🎯 Expert Insight: In mature GitOps workflows, humans rarely run helm install or helm upgrade manually. 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

  1. Practice: Convert one of your existing applications to a Helm chart
  2. Explore: Check out Artifact Hub for community charts
  3. Level Up: Learn about chart dependencies, hooks, and testing with helm test
  4. 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.

Leave a Reply

Your email address will not be published. Required fields are marked *