Back to Blog
Cloud10 min readOctober 5, 2024

Helm Charts: Packaging and Deploying Kubernetes Applications

A practical guide to writing Helm charts for production Kubernetes deployments. Covers chart structure, templating, values files, hooks, chart testing, and publishing to OCI registries.

HelmKubernetesDevOpsInfrastructureCloud
A

Azam

DevOps & AI Consultant

Why Helm Over Raw Kubernetes Manifests

Kubernetes YAML is verbose and repetitive. A single application deployment typically requires a Deployment, Service, Ingress, ConfigMap, HorizontalPodAutoscaler, and PodDisruptionBudget — hundreds of lines of nearly identical YAML across environments, differing only in replicas, image tags, resource limits, and hostnames. Helm templates this YAML and manages the lifecycle: install, upgrade, rollback, and uninstall as atomic operations with a single command.

Chart Structure

my-api/
  Chart.yaml          # chart metadata
  values.yaml         # default configuration values
  templates/
    deployment.yaml
    service.yaml
    ingress.yaml
    hpa.yaml
    _helpers.tpl      # reusable template partials
  charts/             # chart dependencies
  .helmignore
# Chart.yaml
apiVersion: v2
name: my-api
description: Production API service
version: 0.5.0        # chart version
appVersion: "1.0.0"   # application version
dependencies:
  - name: redis
    version: "17.x.x"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled

Deployment Template

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-api.fullname" . }}
  labels:
    {{- include "my-api.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-api.selectorLabels" . | nindent 6 }}
  template:
    spec:
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          ports:
            - containerPort: {{ .Values.service.port }}
          env:
            {{- range .Values.env }}
            - name: {{ .name }}
              value: {{ .value | quote }}
            {{- end }}
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          readinessProbe:
            httpGet:
              path: /health/ready
              port: {{ .Values.service.port }}
            initialDelaySeconds: 5
            periodSeconds: 5

Values Files Per Environment

# values.yaml — defaults
replicaCount: 1
image:
  repository: ghcr.io/org/my-api
  tag: ""
resources:
  requests: { cpu: 100m, memory: 128Mi }
  limits: { cpu: 500m, memory: 512Mi }
autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 10

# values-production.yaml — overrides
replicaCount: 3
resources:
  requests: { cpu: 500m, memory: 512Mi }
  limits: { cpu: 2000m, memory: 2Gi }
autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 20
ingress:
  host: api.production.com
# Deploy to staging
helm upgrade --install my-api ./my-api   --namespace staging   --values values-staging.yaml   --set image.tag=${GIT_SHA}

# Deploy to production
helm upgrade --install my-api ./my-api   --namespace production   --values values-production.yaml   --set image.tag=${GIT_SHA}   --atomic          # rollback automatically if upgrade fails
  --timeout 5m

Helm Hooks for Database Migrations

Helm hooks run Jobs at specific points in the release lifecycle. Use a pre-upgrade hook to run database migrations before the new application pods start.

# templates/migration-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-api.fullname" . }}-migration
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: migration
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
          command: ["npm", "run", "db:migrate"]
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: my-api-secrets
                  key: database-url

Testing Charts with helm-unittest

# tests/deployment_test.yaml
suite: deployment tests
templates:
  - templates/deployment.yaml
tests:
  - it: should set correct replica count
    set:
      replicaCount: 3
    asserts:
      - equal:
          path: spec.replicas
          value: 3

  - it: should use chart appVersion as default image tag
    asserts:
      - matchRegex:
          path: spec.template.spec.containers[0].image
          pattern: ":1.0.0$"

Run chart tests in CI with helm unittest ./my-api before any deployment. Catching broken templates in CI is orders of magnitude cheaper than debugging failed Kubernetes deployments.

Want to Build This for Your Team?

I help teams implement the patterns and architectures described in these articles. Let's talk about your project.

Book a Free Call