Containers

Helm Charts Demystified: Kubernetes Templating Without the Pain

Master Helm charts: chart anatomy, Go templating, values overrides, public registries, release management, lifecycle hooks. Plus when to use Kustomize instead.

A
Abhishek Patel9 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Helm Charts Demystified: Kubernetes Templating Without the Pain
Helm Charts Demystified: Kubernetes Templating Without the Pain

Kubernetes YAML Without the Copy-Paste

Helm is the package manager for Kubernetes. It lets you define, install, and upgrade complex Kubernetes applications using templated YAML called charts. Instead of maintaining dozens of nearly identical manifest files for different environments, you write templates once and inject values at deploy time. It's the difference between managing configuration and fighting it.

If you've ever copied a Kubernetes YAML file, changed three values, and deployed it to staging, you've already felt the pain Helm solves. Charts package your manifests, their defaults, and their dependencies into a single versioned artifact that can be shared, tested, and deployed consistently.

What Is a Helm Chart?

Definition: A Helm chart is a collection of files that describe a related set of Kubernetes resources. It contains YAML templates with Go template syntax, a default values file, metadata, and optional dependencies. Charts are the unit of packaging in Helm -- you install a chart to create a release, which is a running instance of that chart in your cluster.

Chart Anatomy: What's Inside

my-chart/
  Chart.yaml          # Chart metadata (name, version, dependencies)
  values.yaml         # Default configuration values
  charts/             # Dependency charts (subcharts)
  templates/          # Kubernetes manifest templates
    deployment.yaml
    service.yaml
    ingress.yaml
    _helpers.tpl      # Reusable template snippets
    NOTES.txt         # Post-install instructions
  .helmignore         # Files to exclude from packaging

Chart.yaml

The metadata file that defines your chart's identity and dependencies:

apiVersion: v2
name: my-web-app
description: A web application with API and worker
type: application
version: 1.2.0        # Chart version (semver)
appVersion: "3.4.1"   # Application version
dependencies:
  - name: postgresql
    version: "13.x"
    repository: https://charts.bitnami.com/bitnami
    condition: postgresql.enabled

values.yaml

The default configuration that users can override. This is where Helm's power lives -- one file controls everything:

replicaCount: 2
image:
  repository: my-app
  tag: "3.4.1"
  pullPolicy: IfNotPresent

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

ingress:
  enabled: true
  className: nginx
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix

postgresql:
  enabled: true
  auth:
    database: myapp

Go Templating: The Basics You Need

Helm templates use Go's text/template package. The syntax feels odd if you come from Jinja2 or Handlebars, but the core patterns are simple:

# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "my-chart.fullname" . }}
  labels:
    {{- include "my-chart.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "my-chart.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "my-chart.selectorLabels" . | nindent 8 }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        ports:
        - containerPort: {{ .Values.service.port }}
        resources:
          {{- toYaml .Values.resources | nindent 12 }}
        {{- if .Values.env }}
        env:
          {{- range .Values.env }}
          - name: {{ .name }}
            value: {{ .value | quote }}
          {{- end }}
        {{- end }}

Essential Template Functions

FunctionPurposeExample
{{ .Values.x }}Access values.yaml{{ .Values.replicaCount }}
{{ .Release.Name }}Release namemy-app-prod
{{ .Chart.Name }}Chart namemy-web-app
includeRender a named template{{ include "chart.labels" . }}
toYamlConvert value to YAML{{ toYaml .Values.resources }}
nindentNewline + indent{{ x | nindent 4 }}
quoteWrap in quotes{{ .Values.tag | quote }}
defaultFallback value{{ .Values.port | default 8080 }}
if/elseConditional rendering{{- if .Values.ingress.enabled }}
rangeLoop over lists/maps{{- range .Values.env }}

The _helpers.tpl File

This file defines reusable named templates (partials) that other templates reference with include. The convention is to define labels, names, and selectors here:

# templates/_helpers.tpl
{{- define "my-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}

{{- define "my-chart.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ .Chart.Name }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

Working with Helm: Step by Step

  1. Create a chart: helm create my-app generates the scaffold
  2. Edit templates and values to match your application
  3. Lint: helm lint my-app/ checks for errors
  4. Dry run: helm template my-app ./my-app/ -f prod-values.yaml renders YAML without deploying
  5. Install: helm install my-app ./my-app/ -f prod-values.yaml -n production
  6. Upgrade: helm upgrade my-app ./my-app/ -f prod-values.yaml -n production
  7. Rollback: helm rollback my-app 1 -n production reverts to revision 1
# Override values at install time
helm install my-app ./my-app/ \
  --set replicaCount=3 \
  --set image.tag=v2.1.0 \
  -f production-values.yaml \
  -n production

# View all releases
helm list -n production

# Check release history
helm history my-app -n production

Pro tip: Always use helm template to render your manifests locally before deploying. Pipe the output to kubectl diff to see exactly what will change in the cluster. Never deploy blind -- especially to production.

Public Chart Registries

Don't reinvent the wheel. Most common infrastructure (databases, message queues, monitoring tools) has a well-maintained Helm chart:

RegistryURLNotable Charts
Bitnamicharts.bitnami.com/bitnamiPostgreSQL, Redis, Kafka, MongoDB
ingress-nginxkubernetes.github.io/ingress-nginxNGINX Ingress Controller
Jetstackcharts.jetstack.iocert-manager
Prometheus Communityprometheus-community.github.io/helm-chartskube-prometheus-stack
Artifact Hubartifacthub.ioCentral search for all public charts
# Add a repository and install a chart
helm repo add bitnami https://charts.bitnami.com/bitnami
helm repo update
helm install my-postgres bitnami/postgresql -f postgres-values.yaml

Release Management and Lifecycle Hooks

Helm tracks every install and upgrade as a numbered revision. You can roll back to any previous revision, and Helm stores the full manifest for each one.

Lifecycle Hooks

Hooks let you run Jobs or other resources at specific points in the release lifecycle:

# Run a database migration before upgrading
apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-chart.fullname" . }}-migrate
  annotations:
    "helm.sh/hook": pre-upgrade
    "helm.sh/hook-weight": "-1"
    "helm.sh/hook-delete-policy": before-hook-creation
spec:
  template:
    spec:
      containers:
      - name: migrate
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        command: ["node", "migrate.js"]
      restartPolicy: Never

Available Hook Points

  • pre-install / post-install -- run before/after the first install
  • pre-upgrade / post-upgrade -- run before/after upgrades
  • pre-delete / post-delete -- run before/after uninstall
  • pre-rollback / post-rollback -- run before/after rollback
  • test -- run with helm test

Helm 2 vs Helm 3: Key Differences

FeatureHelm 2Helm 3
Tiller (server component)RequiredRemoved
Release storageConfigMaps via TillerSecrets in release namespace
Security modelTiller had cluster-adminUses kubeconfig RBAC
Three-way mergeNo (two-way)Yes (detects manual changes)
Chart API versionv1v2
CRD handlingcrd-install hookcrds/ directory

Watch out: Helm 2 reached end of life in November 2020. If you're still running Tiller, migrate immediately. Tiller runs with cluster-admin permissions by default, which is a serious security risk. Helm 3's removal of Tiller was the single most important change in the project's history.

When to Use Kustomize Instead

Kustomize is Helm's main alternative, built into kubectl. It uses a template-free approach -- you write plain YAML and layer patches on top.

Helm vs Kustomize

AspectHelmKustomize
ApproachTemplating (Go templates)Patching (overlays on base YAML)
PackagingCharts with versioningDirectories with kustomization.yaml
DependenciesBuilt-in subchart systemManual or remote bases
Release trackingBuilt-in revisions + rollbackNone (relies on GitOps)
Learning curveGo templates + Helm CLIPatches + strategic merge
Best forPackaging reusable applicationsEnvironment-specific overlays

Many teams use both: Helm for installing third-party charts (ingress-nginx, cert-manager, Prometheus) and Kustomize for their own application manifests. They're not mutually exclusive.

Pricing and Tooling Costs

ToolCostPurpose
Helm CLIFree (OSS)Chart management and deployment
Artifact HubFreePublic chart discovery
ChartMuseumFree (self-hosted)Private chart repository
AWS ECR (OCI charts)$0.10/GB/monthStore charts as OCI artifacts
HarborFree (OSS)Chart + image registry with scanning
HelmfileFree (OSS)Declarative multi-chart deployments

Frequently Asked Questions

What is the difference between helm install and helm upgrade?

helm install creates a new release -- it fails if the release name already exists. helm upgrade updates an existing release to a new chart version or values. In CI/CD pipelines, use helm upgrade --install which installs if the release doesn't exist and upgrades if it does. This is the idempotent pattern most teams use.

How do I pass secrets to a Helm chart?

Never put secrets in values.yaml or commit them to Git. Use --set with CI/CD pipeline secrets (helm install --set db.password=$DB_PASSWORD), or integrate with external secret management (Sealed Secrets, External Secrets Operator, SOPS-encrypted value files). The helm-secrets plugin decrypts SOPS files at deploy time.

Can I use Helm with GitOps tools like ArgoCD or Flux?

Yes. ArgoCD natively understands Helm charts -- point it at a chart in a Git repo with a values file, and it renders and syncs the manifests. Flux uses HelmRelease CRDs for the same purpose. Both support automatic upgrades when chart versions change. This is the recommended way to use Helm in production.

What are OCI-based Helm charts?

Since Helm 3.8, charts can be stored as OCI (Open Container Initiative) artifacts in any OCI-compatible registry -- Docker Hub, ECR, GCR, GitHub Container Registry. This means you use helm push and helm pull with the same registries that store your container images. No separate chart repository needed.

How do I test Helm charts?

Use helm lint for syntax checks, helm template to render and inspect output, and helm test to run test Pods defined with the test hook annotation. For more thorough testing, tools like chart-testing (ct) from the Helm project validate chart changes in CI, and kubeval or kubeconform validate the rendered YAML against Kubernetes schemas.

Should new projects use Helm or Kustomize?

Use Helm if you're packaging an application for others to install (internal platform team, open-source project) or if you need release tracking and rollback. Use Kustomize if you're deploying your own services and prefer plain YAML with environment overlays. Many teams use Helm for third-party charts and Kustomize for their own apps.

What is Helmfile and when should I use it?

Helmfile is a declarative tool for managing multiple Helm releases. Instead of running helm install commands in scripts, you define all releases in a helmfile.yaml with their values, dependencies, and ordering. Use it when you need to deploy 5+ Helm releases together as a coherent environment. It's the "docker-compose for Helm."

Conclusion

Helm solves a real problem: managing Kubernetes YAML at scale. Start by using public charts for infrastructure (databases, ingress, monitoring), then create your own charts when you find yourself copying manifests between environments. Keep templates simple -- if your _helpers.tpl is longer than your actual templates, you've gone too far. Use helm template and helm lint in CI, store charts as OCI artifacts, and let ArgoCD or Flux handle the actual deployment.

A

Written by

Abhishek Patel

Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.

Related Articles

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.