Helm Charts: Kubernetes Package Management

Kubernetes deployments with Helm package management

Helm has revolutionized Kubernetes application deployment by introducing package management concepts familiar from traditional operating systems.

As Kubernetes adoption grows, managing complex applications with dozens of YAML files becomes challenging. Helm Charts solve this problem by bundling all resources into versioned, configurable packages.

software-developer in the nature This nice image is generated by AI model Flux 1 dev.

Understanding Helm: The Kubernetes Package Manager

Helm is to Kubernetes what apt is to Debian, yum to RedHat, or Homebrew to macOS. It packages Kubernetes applications into Charts – collections of files that describe related Kubernetes resources. A single Chart might deploy a complete application stack: web servers, databases, caching layers, ingress rules, and monitoring components. For those new to Kubernetes, a Kubernetes Cheatsheet provides essential commands and concepts to get started.

Why Helm Matters in Modern DevOps

Complexity Reduction: Instead of managing 20+ YAML files, you manage one Chart with customizable values.

Reproducibility: Deploy identical configurations across development, staging, and production with environment-specific value overrides. This is especially valuable when deploying complex microservices architectures where consistency matters.

Version Control: Charts are versioned, enabling easy rollbacks and upgrade tracking.

Community Ecosystem: Thousands of pre-built Charts available through Artifact Hub (previously Helm Hub) for popular applications like PostgreSQL, Redis, NGINX, Prometheus, and more.

Templating Power: Go templates enable dynamic resource generation based on input values, reducing duplication.

Helm Architecture and Core Concepts

Helm 3 Architecture

Helm 3 simplified the architecture by removing Tiller, the problematic server-side component from Helm 2:

  • Helm Client: CLI tool that interacts directly with Kubernetes API
  • Chart: Package format containing templates and metadata
  • Release: An instance of a Chart running in a Kubernetes cluster
  • Repository: Storage location for Charts (HTTP server or OCI registry)

Key Components of a Helm Chart

my-app-chart/
├── Chart.yaml          # Chart metadata and version
├── values.yaml         # Default configuration values
├── charts/             # Dependent charts
├── templates/          # Kubernetes resource templates
│   ├── deployment.yaml
│   ├── service.yaml
│   ├── ingress.yaml
│   ├── _helpers.tpl    # Template helpers
│   └── NOTES.txt       # Post-installation notes
├── .helmignore         # Files to ignore when packaging
├── README.md
└── LICENSE

Creating Your First Helm Chart

Initialize a New Chart

helm create my-application
cd my-application

This generates a starter Chart with example templates for a deployment, service, and ingress.

Chart.yaml: Defining Metadata

apiVersion: v2
name: my-application
description: A production-ready application chart
type: application
version: 1.0.0        # Chart version
appVersion: "2.4.1"   # Version of the application

maintainers:
  - name: Your Team
    email: team@company.com

dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "https://charts.bitnami.com/bitnami"
    condition: postgresql.enabled

Values.yaml: Configuration Management

The values.yaml file defines default configurations that users can override. This approach separates configuration from templates, making it easy to manage different environments (development, staging, production) and customize deployments without modifying template files.

replicaCount: 3

image:
  repository: myapp/backend
  tag: "1.0.0"
  pullPolicy: IfNotPresent

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: true
  className: "nginx"
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix

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

autoscaling:
  enabled: true
  minReplicas: 3
  maxReplicas: 10
  targetCPUUtilizationPercentage: 80

Templates: Dynamic Kubernetes Manifests

Templates use Go templating syntax to generate Kubernetes resources dynamically. These templates can generate any Kubernetes resource type, from simple Deployments to complex StatefulSets for stateful applications requiring persistent storage. For applications that need stable identities and persistent volumes, you’ll want to use StatefulSets instead of Deployments, as detailed in our guide on StatefulSets and Persistent Storage in Kubernetes.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-application.fullname" . }}
  labels:
    {{- include "my-application.labels" . | nindent 4 }}
spec:
  {{- if not .Values.autoscaling.enabled }}
  replicas: {{ .Values.replicaCount }}
  {{- end }}
  selector:
    matchLabels:
      {{- include "my-application.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-application.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
        imagePullPolicy: {{ .Values.image.pullPolicy }}
        ports:
        - containerPort: 8080
          protocol: TCP
        resources:
          {{- toYaml .Values.resources | nindent 10 }}

Template Helpers (_helpers.tpl)

Create reusable template functions to avoid repetition:

{{/*
Expand the name of the chart.
*/}}
{{- define "my-application.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Create a default fully qualified app name.
*/}}
{{- define "my-application.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 "my-application.labels" -}}
helm.sh/chart: {{ include "my-application.chart" . }}
{{ include "my-application.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

Managing Helm Charts: Installation and Operations

Installing a Chart

# Install from a repository
helm install my-release bitnami/postgresql

# Install from local directory
helm install my-app ./my-application

# Install with custom values
helm install my-app ./my-application -f values-production.yaml

# Install with inline value overrides
helm install my-app ./my-application \
  --set replicaCount=5 \
  --set image.tag=2.0.0

Upgrading Releases

# Upgrade with new values
helm upgrade my-app ./my-application -f values-production.yaml

# Upgrade with atomic rollback on failure
helm upgrade my-app ./my-application --atomic --timeout 5m

# Force resource updates
helm upgrade my-app ./my-application --force

Rollback and History

# View release history
helm history my-app

# Rollback to previous version
helm rollback my-app

# Rollback to specific revision
helm rollback my-app 3

Testing and Debugging

# Dry-run to see generated manifests
helm install my-app ./my-application --dry-run --debug

# Template rendering without installation
helm template my-app ./my-application

# Lint chart for issues
helm lint ./my-application

# Test release with test hooks
helm test my-app

Advanced Helm Features

Chart Dependencies

Helm Charts can depend on other Charts, allowing you to compose complex applications from reusable components. This is particularly useful when deploying microservices that need databases, message queues, or other supporting services. Define dependencies in Chart.yaml:

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

Update dependencies:

helm dependency update ./my-application

Helm Hooks for Lifecycle Management

Hooks execute at specific points in the release lifecycle:

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-application.fullname" . }}-db-migration
  annotations:
    "helm.sh/hook": pre-upgrade,pre-install
    "helm.sh/hook-weight": "5"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      containers:
      - name: db-migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        command: ["python", "manage.py", "migrate"]
      restartPolicy: Never

Hook types include:

  • pre-install: Before resources are installed
  • post-install: After all resources are installed
  • pre-upgrade: Before upgrade
  • post-upgrade: After upgrade
  • pre-delete: Before deletion
  • post-delete: After deletion
  • pre-rollback: Before rollback
  • post-rollback: After rollback

Conditional Logic and Flow Control

{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: {{ include "my-application.fullname" . }}
  {{- with .Values.ingress.annotations }}
  annotations:
    {{- toYaml . | nindent 4 }}
  {{- end }}
spec:
  {{- if .Values.ingress.className }}
  ingressClassName: {{ .Values.ingress.className }}
  {{- end }}
  rules:
  {{- range .Values.ingress.hosts }}
  - host: {{ .host | quote }}
    http:
      paths:
      {{- range .paths }}
      - path: {{ .path }}
        pathType: {{ .pathType }}
        backend:
          service:
            name: {{ include "my-application.fullname" $ }}
            port:
              number: {{ $.Values.service.port }}
      {{- end }}
  {{- end }}
{{- end }}

OCI Registry Support: Modern Chart Distribution

Since Helm 3.8, OCI (Open Container Initiative) registry support is stable, allowing Charts to be stored alongside container images.

Publishing to OCI Registry

# Login to registry
helm registry login registry.example.com

# Package the chart
helm package ./my-application

# Push to OCI registry
helm push my-application-1.0.0.tgz oci://registry.example.com/charts

# Install from OCI registry
helm install my-app oci://registry.example.com/charts/my-application --version 1.0.0

Benefits of OCI Registries

  • Unified Storage: Charts and images in one place
  • Standard Tooling: Use existing registry infrastructure
  • Better Security: Leverage registry authentication and scanning
  • Simpler Management: No separate Chart repository server needed

Best Practices for Production Helm Charts

1. Values Structure and Documentation

Document all values with comments:

# -- Number of replicas for the application
replicaCount: 3

# -- Image configuration
image:
  # -- Image repository
  repository: myapp/backend
  # -- Image pull policy
  pullPolicy: IfNotPresent
  # -- Image tag (defaults to chart appVersion)
  tag: ""

2. Resource Management

Always set resource requests and limits:

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

3. Security Contexts

Define security contexts for containers:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  fsGroup: 1000
  capabilities:
    drop:
    - ALL
  readOnlyRootFilesystem: true

4. Health Checks

Include liveness and readiness probes:

livenessProbe:
  httpGet:
    path: /health
    port: 8080
  initialDelaySeconds: 30
  periodSeconds: 10

readinessProbe:
  httpGet:
    path: /ready
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

5. ConfigMap and Secret Management

Proper secret management is critical for production deployments. Use external secret managers (Sealed Secrets, External Secrets Operator, or Vault) rather than storing secrets in values files. This ensures sensitive data like database passwords, API keys, and certificates are handled securely:

envFrom:
- secretRef:
    name: {{ include "my-application.fullname" . }}-secrets
- configMapRef:
    name: {{ include "my-application.fullname" . }}-config

6. Schema Validation

Create values.schema.json to validate user inputs:

{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "properties": {
    "replicaCount": {
      "type": "integer",
      "minimum": 1
    },
    "image": {
      "type": "object",
      "properties": {
        "repository": {
          "type": "string"
        },
        "tag": {
          "type": "string"
        }
      },
      "required": ["repository"]
    }
  },
  "required": ["image"]
}

7. NOTES.txt for User Guidance

Provide post-installation instructions:

1. Get the application URL by running:
{{- if .Values.ingress.enabled }}
  https://{{ (index .Values.ingress.hosts 0).host }}
{{- else if contains "NodePort" .Values.service.type }}
  export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "my-application.fullname" . }})
  export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}")
  echo http://$NODE_IP:$NODE_PORT
{{- end }}

2. Monitor the deployment:
  kubectl --namespace {{ .Release.Namespace }} get pods -l "app.kubernetes.io/name={{ include "my-application.name" . }}"

Helm Chart Testing and CI/CD Integration

Chart Testing with chart-testing (ct)

# Install chart-testing
brew install chart-testing

# Lint charts
ct lint --config ct.yaml

# Install and test charts
ct install --config ct.yaml

GitHub Actions Example

name: Helm Chart CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  lint-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0

      - name: Set up Helm
        uses: azure/setup-helm@v3
        with:
          version: 3.12.0

      - name: Set up chart-testing
        uses: helm/chart-testing-action@v2

      - name: Lint charts
        run: ct lint --config ct.yaml

      - name: Create kind cluster
        uses: helm/kind-action@v1

      - name: Install charts
        run: ct install --config ct.yaml

GitOps with Helm: ArgoCD and Flux

GitOps tools like ArgoCD and Flux integrate seamlessly with Helm Charts, enabling declarative, automated deployments. These tools watch your Git repository for changes and automatically sync Helm releases, making continuous deployment straightforward. For complex microservices architectures, consider how distributed transaction patterns like the Saga pattern can help manage consistency across services deployed via Helm.

ArgoCD Application

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: my-application
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/my-app-chart
    targetRevision: main
    path: charts/my-application
    helm:
      valueFiles:
        - values-production.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Flux HelmRelease

apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
  name: my-application
  namespace: flux-system
spec:
  interval: 5m
  chart:
    spec:
      chart: my-application
      version: '1.x.x'
      sourceRef:
        kind: HelmRepository
        name: my-charts
  values:
    replicaCount: 5
    image:
      tag: "2.0.0"

Troubleshooting Common Helm Issues

Issue: Failed Upgrade Stuck in Pending

# Check release status
helm list --all-namespaces

# Get release details
helm status my-app -n namespace

# Force delete if necessary (use carefully)
kubectl delete secret -n namespace -l owner=helm,name=my-app

Issue: Template Rendering Errors

# Debug template rendering
helm template my-app ./my-application --debug

# Validate against Kubernetes
helm template my-app ./my-application | kubectl apply --dry-run=client -f -

Issue: Values Not Applied

# Check merged values
helm get values my-app

# Show all computed values
helm get values my-app --all

Helm Ecosystem and Tools

The Helm ecosystem includes numerous tools that extend its capabilities and integrate with other Kubernetes technologies. When deploying applications that require advanced networking features like service-to-service communication, traffic management, and security policies, consider integrating with a Service Mesh with Istio and Linkerd, which can be deployed and managed through Helm Charts.

Essential Tools

  • Helmfile: Declarative spec for deploying Helm Charts
  • Helm Diff: Preview changes before upgrade
  • Helm Secrets: Manage secrets with SOPS
  • Nova: Find outdated Helm Charts
  • Pluto: Detect deprecated Kubernetes APIs

Example Helmfile

repositories:
  - name: bitnami
    url: https://charts.bitnami.com/bitnami

releases:
  - name: postgresql
    namespace: database
    chart: bitnami/postgresql
    version: 12.x.x
    values:
      - postgresql:
          auth:
            database: myapp
            username: appuser

  - name: my-app
    namespace: production
    chart: ./charts/my-application
    values:
      - values-production.yaml
    needs:
      - database/postgresql

The Future of Helm

The Helm ecosystem continues evolving:

  • OCI-Native: Full transition to OCI registries as the standard
  • Improved Security: Better secret management and signing capabilities
  • Performance: Faster rendering and installation for large Charts
  • WASM Support: WebAssembly for Chart plugins and extensions
  • Better Validation: Enhanced schema validation and policy enforcement

Conclusion

Helm has become the de facto standard for Kubernetes package management, and mastering it is essential for modern DevOps practices. By understanding Chart structure, templating, values management, and best practices, you can create maintainable, secure, and scalable Kubernetes deployments.

Start with simple Charts, gradually incorporate advanced features like hooks and dependencies, and integrate with GitOps tools for production-grade infrastructure. The Helm community provides thousands of pre-built Charts, but the real power comes from creating custom Charts tailored to your organization’s needs. Whether you’re deploying stateless applications or stateful workloads requiring persistent storage, Helm simplifies the complexity of Kubernetes resource management. For teams setting up new Kubernetes clusters, consider installing Kubernetes with Kubespray or exploring Kubernetes distributions like k3s or MicroK8s for development environments. If you’re evaluating which distribution fits your homelab or small cluster needs, see our comprehensive comparison of Kubernetes distributions for detailed analysis.