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.
Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

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
| Function | Purpose | Example |
|---|---|---|
{{ .Values.x }} | Access values.yaml | {{ .Values.replicaCount }} |
{{ .Release.Name }} | Release name | my-app-prod |
{{ .Chart.Name }} | Chart name | my-web-app |
include | Render a named template | {{ include "chart.labels" . }} |
toYaml | Convert value to YAML | {{ toYaml .Values.resources }} |
nindent | Newline + indent | {{ x | nindent 4 }} |
quote | Wrap in quotes | {{ .Values.tag | quote }} |
default | Fallback value | {{ .Values.port | default 8080 }} |
if/else | Conditional rendering | {{- if .Values.ingress.enabled }} |
range | Loop 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
- Create a chart:
helm create my-appgenerates the scaffold - Edit templates and values to match your application
- Lint:
helm lint my-app/checks for errors - Dry run:
helm template my-app ./my-app/ -f prod-values.yamlrenders YAML without deploying - Install:
helm install my-app ./my-app/ -f prod-values.yaml -n production - Upgrade:
helm upgrade my-app ./my-app/ -f prod-values.yaml -n production - Rollback:
helm rollback my-app 1 -n productionreverts 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 templateto render your manifests locally before deploying. Pipe the output tokubectl diffto 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:
| Registry | URL | Notable Charts |
|---|---|---|
| Bitnami | charts.bitnami.com/bitnami | PostgreSQL, Redis, Kafka, MongoDB |
| ingress-nginx | kubernetes.github.io/ingress-nginx | NGINX Ingress Controller |
| Jetstack | charts.jetstack.io | cert-manager |
| Prometheus Community | prometheus-community.github.io/helm-charts | kube-prometheus-stack |
| Artifact Hub | artifacthub.io | Central 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 installpre-upgrade/post-upgrade-- run before/after upgradespre-delete/post-delete-- run before/after uninstallpre-rollback/post-rollback-- run before/after rollbacktest-- run withhelm test
Helm 2 vs Helm 3: Key Differences
| Feature | Helm 2 | Helm 3 |
|---|---|---|
| Tiller (server component) | Required | Removed |
| Release storage | ConfigMaps via Tiller | Secrets in release namespace |
| Security model | Tiller had cluster-admin | Uses kubeconfig RBAC |
| Three-way merge | No (two-way) | Yes (detects manual changes) |
| Chart API version | v1 | v2 |
| CRD handling | crd-install hook | crds/ 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
| Aspect | Helm | Kustomize |
|---|---|---|
| Approach | Templating (Go templates) | Patching (overlays on base YAML) |
| Packaging | Charts with versioning | Directories with kustomization.yaml |
| Dependencies | Built-in subchart system | Manual or remote bases |
| Release tracking | Built-in revisions + rollback | None (relies on GitOps) |
| Learning curve | Go templates + Helm CLI | Patches + strategic merge |
| Best for | Packaging reusable applications | Environment-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
| Tool | Cost | Purpose |
|---|---|---|
| Helm CLI | Free (OSS) | Chart management and deployment |
| Artifact Hub | Free | Public chart discovery |
| ChartMuseum | Free (self-hosted) | Private chart repository |
| AWS ECR (OCI charts) | $0.10/GB/month | Store charts as OCI artifacts |
| Harbor | Free (OSS) | Chart + image registry with scanning |
| Helmfile | Free (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.
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
Certificate Management at Scale: Let's Encrypt, ACME, and cert-manager
Automate TLS certificates with Let's Encrypt, ACME protocol, and cert-manager in Kubernetes. Covers HTTP-01, DNS-01, wildcards, private CAs, and expiry monitoring.
9 min read
SecuritySecret Management: HashiCorp Vault vs AWS Secrets Manager vs Kubernetes Secrets
Compare Vault, AWS Secrets Manager, and Kubernetes Secrets. Learn about dynamic secrets, rotation, injection patterns, and when to use each tool.
9 min read
ContainersKubernetes Pods, Deployments, and Services: A Visual Guide
Kubernetes complexity concentrates in three core objects: Pods, Deployments, and Services. This visual guide explains what they do, how they connect, and what happens during rolling updates.
11 min read
Enjoyed this article?
Get more like this in your inbox. No spam, unsubscribe anytime.