Skip to content

I'm currently populating my catalog on the site. Pardon the prefilled data. The entries are actively being updated and cleaned up.

Previous website

Helm Explained Properly: Why Kubernetes Needed It and How It Makes Life Easier

A practical guide to what Helm solves in Kubernetes, how charts and values work, and why releases, upgrades, and rollbacks make real operations easier.

2026-04-2314 min read
  • Kubernetes
  • Helm
  • CI/CD

Why this matters#

When people first learn Kubernetes, they usually start the hard way:

  • writing raw YAML
  • applying one manifest at a time
  • managing Deployments, Services, Ingress, ConfigMaps, Secrets, RBAC, Jobs, and HPAs separately
  • copying and editing almost-identical files across environments

At first, this feels normal.

But once your app becomes even a little realistic, Kubernetes YAML starts to become hard to manage, hard to reuse, and easy to break.

That pain is exactly why Helm matters.

This article focuses on Helm first, not Kubernetes broadly.

The goal is to help you deeply understand:

  • what life looks like without Helm
  • what Helm actually solves
  • how charts, values, releases, upgrades, rollbacks, hooks, and dependencies work
  • how Helm is used in a real project
  • why Helm makes Kubernetes significantly easier to operate

I will use lots of examples and code, but before each code block, I will explain what problem that section is solving so it does not feel like random syntax.

Before Helm: What Kubernetes Feels Like Without It#

Let us start with the old-school approach.

Suppose you have a simple web application with:

  • a backend API
  • a frontend
  • an Ingress
  • environment variables
  • secrets
  • autoscaling
  • a ServiceAccount
  • monitoring later on

Without Helm, you often manage everything as separate YAML files.

That means your repo might look like this:

The difference is not just fewer commands. Helm turns a scattered set of manifests into one reusable deployment package.
BASH
my-app/├── backend/│   ├── Dockerfile│   └── src/├── frontend/│   ├── Dockerfile│   └── src/├── k8s/│   ├── namespace.yaml│   ├── backend-deployment.yaml│   ├── backend-service.yaml│   ├── backend-configmap.yaml│   ├── backend-secret.yaml│   ├── backend-hpa.yaml│   ├── frontend-deployment.yaml│   ├── frontend-service.yaml│   ├── frontend-configmap.yaml│   ├── ingress.yaml│   ├── serviceaccount.yaml│   ├── role.yaml│   ├── rolebinding.yaml│   └── networkpolicy.yaml└── README.md

At first glance, this looks manageable.

But the difficulty shows up when you actually have to work with it.

The real pain of managing raw manifests#

Without Helm, you usually do things like this:

BASH
kubectl apply -f k8s/namespace.yamlkubectl apply -f k8s/backend-configmap.yamlkubectl apply -f k8s/backend-secret.yamlkubectl apply -f k8s/backend-deployment.yamlkubectl apply -f k8s/backend-service.yamlkubectl apply -f k8s/backend-hpa.yamlkubectl apply -f k8s/frontend-configmap.yamlkubectl apply -f k8s/frontend-deployment.yamlkubectl apply -f k8s/frontend-service.yamlkubectl apply -f k8s/ingress.yamlkubectl apply -f k8s/serviceaccount.yamlkubectl apply -f k8s/role.yamlkubectl apply -f k8s/rolebinding.yamlkubectl apply -f k8s/networkpolicy.yaml

This works.

But now imagine you need to:

  • deploy the same app to dev, staging, and prod
  • change image tags for each environment
  • give prod 5 replicas and dev 1 replica
  • use a different hostname per environment
  • use different secrets
  • add monitoring
  • track which exact version was deployed
  • roll back safely when a deployment fails

This is where raw manifests become painful.

Why raw Kubernetes manifests get hard to manage#

The problem is not that YAML is bad.

The problem is that Kubernetes YAML becomes repetitive and scattered very quickly.

Here is a simple backend Deployment without Helm:

YAML
apiVersion: apps/v1kind: Deploymentmetadata:  name: backend  namespace: my-appspec:  replicas: 2  selector:    matchLabels:      app: backend  template:    metadata:      labels:        app: backend    spec:      containers:        - name: backend          image: myrepo/backend:v1          ports:            - containerPort: 8080          envFrom:            - configMapRef:                name: backend-config

Looks fine.

Now imagine you need the same file for staging and prod, but with different:

  • image tag
  • namespace
  • hostname
  • resources
  • replica count
  • secrets
  • environment variables

What often happens is this:

BASH
k8s/├── dev/│   ├── backend-deployment.yaml│   ├── backend-service.yaml│   ├── ingress.yaml│   └── ...├── staging/│   ├── backend-deployment.yaml│   ├── backend-service.yaml│   ├── ingress.yaml│   └── ...└── prod/    ├── backend-deployment.yaml    ├── backend-service.yaml    ├── ingress.yaml    └── ...

Now you have duplication.

And duplication creates problems:

  • one environment gets updated, another does not
  • copy-paste errors creep in
  • version tracking becomes unclear
  • rollback becomes manual and messy
  • the bigger the app gets, the worse it gets

This is the pain Helm is designed to solve.

So what is Helm, really?#

Helm is often introduced as:

The package manager for Kubernetes.

That is true, but it is incomplete.

A more useful explanation is this:

Helm is a tool that helps you package, template, install, upgrade, and manage Kubernetes applications as a single unit.

In practice, Helm helps with five major things:

  • grouping many Kubernetes resources together
  • templating them so they are reusable
  • customizing them using values files
  • tracking deployments as releases
  • rolling back when something breaks

So instead of managing dozens of separate manifests manually, Helm lets you manage an application as one organized deployment unit.

The shift in mental model: from loose files to one managed application#

Without Helm, your mindset is:

I have many Kubernetes YAML files.

With Helm, your mindset becomes:

I have one application definition, and Helm renders the right Kubernetes YAML for me.

That is a huge shift.

And it is what makes Kubernetes feel much more manageable in real-world environments.

The simplest Helm example#

Let us compare raw Kubernetes vs Helm in one small example.

Without Helm:

BASH
kubectl apply -f deployment.yamlkubectl apply -f service.yamlkubectl apply -f ingress.yamlkubectl apply -f configmap.yamlkubectl apply -f secret.yaml

With Helm:

BASH
helm install my-app ./chart

That single Helm command can create all of those resources together.

But the real win is not just fewer commands.

The real win is that Helm also:

  • remembers what was deployed
  • lets you customize it by environment
  • supports upgrades cleanly
  • lets you roll back
  • gives you structure

The core idea behind a Helm chart#

A Helm chart is the package that defines your Kubernetes application.

Think of it like a reusable application template.

A chart typically contains:

  • metadata about the chart
  • default configuration values
  • templates for Kubernetes manifests

A simple chart looks like this:

Helm combines chart templates with values and a release name to render final Kubernetes manifests before applying them.
BASH
mychart/├── Chart.yaml├── values.yaml├── templates/│   ├── deployment.yaml│   ├── service.yaml│   ├── ingress.yaml│   └── configmap.yaml

Let us break this down.

Chart.yaml - chart metadata#

This file describes the chart itself.

Example:

YAML
apiVersion: v2name: my-appdescription: Helm chart for my applicationtype: applicationversion: 0.1.0appVersion: "1.0.0"

What this means:

  • name: chart name
  • version: chart version
  • appVersion: app version the chart is deploying

This distinction matters because:

  • chart version = version of deployment package/template
  • app version = version of the actual app container/software

values.yaml - default settings#

This file contains default values for the chart.

Example:

YAML
replicaCount: 2 image:  repository: myrepo/backend  tag: "v1"  pullPolicy: IfNotPresent service:  type: ClusterIP  port: 80 containerPort: 8080 ingress:  enabled: true  host: myapp.example.com

This file is important because it separates configuration from templates.

Instead of hardcoding everything in manifest files, you define configurable values here.

templates/ - Kubernetes manifests with placeholders#

This folder contains templated Kubernetes YAML.

For example, instead of hardcoding replica count and image tag, you reference values.

A Deployment template might look like this:

YAML
apiVersion: apps/v1kind: Deploymentmetadata:  name: {{ .Release.Name }}-backendspec:  replicas: {{ .Values.replicaCount }}  selector:    matchLabels:      app: {{ .Release.Name }}-backend  template:    metadata:      labels:        app: {{ .Release.Name }}-backend    spec:      containers:        - name: backend          image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"          imagePullPolicy: {{ .Values.image.pullPolicy }}          ports:            - containerPort: {{ .Values.containerPort }}

Now the YAML is no longer static.

It becomes reusable.

Helm will replace those placeholders using the values you provide.

That is the core magic of Helm.

Why templating matters so much#

Templating solves one of the biggest problems in Kubernetes:

you do not want to keep copying nearly identical YAML files for every environment

With templating:

  • the structure stays the same
  • only the values change

So you can use the same chart for:

  • dev
  • staging
  • prod

without duplicating your whole manifest set.

Custom values: how Helm makes one chart reusable across environments#

The built-in values.yaml is only the default.

In real projects, you usually create environment-specific values files outside the chart.

For example:

BASH
my-project/├── helm/│   └── my-app/│       ├── Chart.yaml│       ├── values.yaml│       └── templates/├── values/│   ├── dev.yaml│   ├── staging.yaml│   └── prod.yaml

This is one of the most important Helm best practices.

You usually do not edit the chart's default values file for each environment.

Instead, you keep environment overrides separate.

For example, values/dev.yaml might look like this:

YAML
replicaCount: 1 image:  tag: "dev-42" ingress:  host: dev.myapp.example.com

And values/prod.yaml might look like this:

YAML
replicaCount: 5 image:  tag: "v1.3.2" ingress:  host: myapp.example.com

Now the same chart can behave differently depending on which file you use.

Install in dev:

BASH
helm install my-app ./helm/my-app -f values/dev.yaml

Install in prod:

BASH
helm install my-app ./helm/my-app -f values/prod.yaml

Same chart. Different behavior.

That is a major reason Helm makes life easier.

Releases: the Helm feature that students often miss#

When you install a chart, Helm creates something called a release.

A release is Helm's record of a deployed instance of that chart.

Example:

BASH
helm install my-app ./helm/my-app -f values/prod.yaml

In this command:

  • my-app is the release name
  • ./helm/my-app is the chart
  • values/prod.yaml customizes the chart

Why does this matter?

Because Helm now tracks that deployment as a manageable unit.

You can inspect it, upgrade it, roll it back, and view its history.

Helm releases make upgrades and rollbacks feel like controlled operations instead of panic-driven guesswork.

List releases:

BASH
helm list

Check release history:

BASH
helm history my-app

Example output:

BASH
REVISION  STATUS     CHART         APP VERSION1         deployed   my-app-0.1.0  1.0.02         deployed   my-app-0.2.0  1.1.0

This is far more organized than manually applying dozens of files and hoping you remember what changed.

Upgrades: what Helm actually upgrades#

A common question is:

What exactly is being upgraded?

You are usually upgrading one or both of these:

  • the chart version
  • the values being applied

Maybe:

  • your app image tag changed
  • you added an Ingress setting
  • you changed replica count
  • the chart templates were improved
  • you enabled a new feature

Example upgrade:

BASH
helm upgrade my-app ./helm/my-app -f values/prod.yaml

What Helm does:

  1. loads the chart
  2. loads the values file
  3. renders the final Kubernetes manifests
  4. compares them with what is already deployed
  5. updates the resources that changed

This is one reason Helm feels much safer and cleaner than manually re-applying disconnected files.

Rollbacks: one of the biggest operational benefits of Helm#

Imagine a deployment goes wrong.

Without Helm, rollback can be ugly:

  • find old manifests
  • find old image tags
  • remember what changed
  • reapply older files manually
  • hope it matches the previous state

With Helm, rollback is built in.

Check history:

BASH
helm history my-app

Roll back to revision 1:

BASH
helm rollback my-app 1

That is incredibly powerful in real operations.

It reduces panic.

It reduces guesswork.

And it gives teams confidence to move faster.

A realistic project structure without Helm vs with Helm#

Now let us compare project organization more clearly.

Without Helm#

This is common in early Kubernetes projects:

BASH
my-project/├── backend/│   ├── Dockerfile│   └── src/├── frontend/│   ├── Dockerfile│   └── src/├── k8s/│   ├── dev/│   │   ├── backend-deployment.yaml│   │   ├── backend-service.yaml│   │   ├── frontend-deployment.yaml│   │   ├── frontend-service.yaml│   │   ├── ingress.yaml│   │   ├── configmap.yaml│   │   ├── secret.yaml│   │   └── hpa.yaml│   ├── staging/│   │   ├── backend-deployment.yaml│   │   ├── backend-service.yaml│   │   ├── frontend-deployment.yaml│   │   ├── frontend-service.yaml│   │   ├── ingress.yaml│   │   ├── configmap.yaml│   │   ├── secret.yaml│   │   └── hpa.yaml│   └── prod/│       ├── backend-deployment.yaml│       ├── backend-service.yaml│       ├── frontend-deployment.yaml│       ├── frontend-service.yaml│       ├── ingress.yaml│       ├── configmap.yaml│       ├── secret.yaml│       └── hpa.yaml└── README.md

Why this gets hard:

  • huge duplication
  • environment drift
  • hard to reuse
  • hard to standardize
  • harder rollback story
  • hard to package as one deployable unit

With Helm#

Now compare that with a Helm-based structure:

BASH
my-project/├── backend/│   ├── Dockerfile│   └── src/├── frontend/│   ├── Dockerfile│   └── src/├── helm/│   └── my-app/│       ├── Chart.yaml│       ├── values.yaml│       ├── templates/│       │   ├── backend-deployment.yaml│       │   ├── backend-service.yaml│       │   ├── frontend-deployment.yaml│       │   ├── frontend-service.yaml│       │   ├── ingress.yaml│       │   ├── configmap.yaml│       │   ├── secret.yaml│       │   └── hpa.yaml│       └── charts/├── values/│   ├── dev.yaml│   ├── staging.yaml│   └── prod.yaml├── ci/│   ├── Jenkinsfile│   └── deploy.sh└── README.md

Why this is better:

  • one reusable chart
  • cleaner environment separation
  • less duplication
  • better standardization
  • release management built in
  • rollback built in
  • easier automation in CI/CD

That is the real practical value of Helm.

Installing third-party apps becomes much easier with Helm#

Another big reason Helm matters is that you do not always want to build everything yourself.

Sometimes you need to install tools like:

  • Prometheus
  • Grafana
  • NGINX Ingress Controller
  • cert-manager
  • ExternalDNS
  • AWS Load Balancer Controller

Without Helm, installing these manually can mean reading dozens of manifests and wiring them together yourself.

With Helm, you can often install them from maintained charts.

First add a repository:

BASH
helm repo add grafana https://grafana.github.io/helm-chartshelm repo update

Then install Grafana:

BASH
helm install my-grafana grafana/grafana

That is dramatically easier than manually assembling all required manifests yourself.

This is one reason Helm is so widely used in real Kubernetes environments.

What "adding a Helm repo" actually means#

This confuses many beginners at first.

When you add a Helm repository, you are not installing an app yet.

You are simply telling Helm:

Here is a catalog of charts I want you to know about.

It is very similar to adding a package repository in Linux.

Example:

BASH
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts

Now Helm knows where to find charts published there.

You can then search or install charts from that source.

Search repositories:

BASH
helm search repo prometheus

That makes Helm feel a lot like package management systems students may already know.

Hooks: how Helm adds lifecycle automation#

Sometimes deployment is not just create resources.

Sometimes you need something to happen:

  • before install
  • after install
  • before upgrade
  • after upgrade

For example:

  • run database migrations
  • perform setup checks
  • create initialization jobs

This is where hooks come in.

Hooks are just Kubernetes manifests with Helm annotations that tell Helm when to run them.

Example pre-install and pre-upgrade hook:

YAML
apiVersion: batch/v1kind: Jobmetadata:  name: "{{ .Release.Name }}-init"  annotations:    "helm.sh/hook": pre-install,pre-upgradespec:  template:    spec:      containers:        - name: init          image: busybox          command: ["sh", "-c", "echo preparing..."]      restartPolicy: Never

Where does this file go?

Inside the chart's templates/ directory, usually as its own file.

For example:

BASH
helm/my-app/├── Chart.yaml├── values.yaml└── templates/    ├── deployment.yaml    ├── service.yaml    ├── ingress.yaml    └── pre-install-job.yaml

Why hooks matter:

They let Helm coordinate more than just static resource creation.

They let Helm participate in the deployment lifecycle.

Dependencies: when one chart uses another chart#

Sometimes your application depends on another packaged component.

For example:

  • your app may depend on Redis
  • a platform chart may include Grafana
  • a monitoring chart may include Alertmanager

Helm supports this through dependencies.

These are usually defined in Chart.yaml.

Example:

YAML
apiVersion: v2name: my-platformversion: 0.1.0 dependencies:  - name: grafana    version: "8.x.x"    repository: "https://grafana.github.io/helm-charts"

Then fetch dependencies:

BASH
helm dependency update ./helm/my-platform

This pulls dependency charts into the chart's dependency area so they can be installed together.

This is useful because it encourages reuse instead of rebuilding common components from scratch.

Helm debugging: an underrated part of working safely#

One mistake beginners make is assuming Helm is magic.

It is not.

Helm is most useful when you inspect what it will generate.

Two commands matter a lot here.

Lint your chart#

This checks for common chart issues.

BASH
helm lint ./helm/my-app

Render templates locally#

This shows the final Kubernetes YAML Helm would send to the cluster.

BASH
helm template my-app ./helm/my-app -f values/dev.yaml

This is incredibly useful because it helps you see:

  • whether placeholders render correctly
  • whether values are being applied correctly
  • what Kubernetes will actually receive

In other words, Helm is easier when you do not treat it as a black box.

Helm in CI/CD: where it really shines operationally#

Helm becomes even more valuable when used in pipelines.

A simple deployment flow might be:

  1. build container image
  2. push image to registry
  3. deploy using Helm with the right values file

Example:

BASH
# Build imagedocker build -t myrepo/my-app:v1.3.2 . # Push imagedocker push myrepo/my-app:v1.3.2 # Deploy to Kuberneteshelm upgrade --install my-app ./helm/my-app \  -f values/prod.yaml \  --set image.tag=v1.3.2

Why this is powerful:

  • same chart structure reused every deploy
  • versioned deployment behavior
  • easier rollback
  • easy environment switching
  • much better fit for automation

This is why Helm is so common in real engineering teams.

What Helm does not replace#

It is also important to be clear about what Helm is not.

Helm does not replace Kubernetes.

Helm does not run containers.

Helm does not schedule Pods.

Helm does not replace the API server.

Helm helps you define and manage what gets sent to Kubernetes.

So the right mental model is:

Kubernetes is the system that runs the application. Helm is the packaging and deployment management layer on top of it.

That distinction helps a lot.

A complete learning summary for students#

Here is the cleanest way to think about Helm after everything we have covered.

Without Helm:

  • many separate YAML files
  • manual application
  • duplication across environments
  • harder upgrades
  • harder rollbacks
  • harder reuse

With Helm:

  • one chart packages the app
  • values customize behavior
  • releases track deployments
  • upgrades are structured
  • rollbacks are built in
  • third-party tools are easier to install
  • CI/CD becomes cleaner

That is why Helm matters.

It does not make Kubernetes simple in the sense of removing complexity.

It makes Kubernetes manageable.

And in real engineering, that matters more.

Final takeaway#

If Kubernetes by itself feels like:

a pile of YAML files

then Helm helps turn it into:

a structured, reusable, versioned application deployment

That is the real win.

What I recommend you do next#

If you want this knowledge to stick, do this hands-on:

  1. create a very small app chart
  2. add a Deployment and Service template
  3. create dev.yaml and prod.yaml
  4. install with one values file
  5. upgrade using a new image tag
  6. check release history
  7. roll back to the previous revision
  8. install a third-party chart like Grafana from a Helm repo

That sequence will make Helm click much faster than memorizing commands.

Handy command recap#

Use this section as a quick reference.

Add a chart repository#

BASH
helm repo add grafana https://grafana.github.io/helm-chartshelm repo update

Search for charts#

BASH
helm search repo grafana

Install a chart#

BASH
helm install my-app ./helm/my-app -f values/dev.yaml

Upgrade a release#

BASH
helm upgrade my-app ./helm/my-app -f values/prod.yaml

Upgrade or install if missing#

BASH
helm upgrade --install my-app ./helm/my-app -f values/prod.yaml

View releases#

BASH
helm list

View release history#

BASH
helm history my-app

Roll back#

BASH
helm rollback my-app 1

Lint chart#

BASH
helm lint ./helm/my-app

Render manifests locally#

BASH
helm template my-app ./helm/my-app -f values/dev.yaml

Fetch dependencies#

BASH
helm dependency update ./helm/my-app

Related Posts

Additional notes connected to the same operating and platform engineering themes.

Kubernetes Operators: The Missing Piece After Helm

A practical first-principles guide to Kubernetes Operators, why Helm only solves Day 1 deployment, and how operators encode Day 2 operational knowledge into the cluster.

  • Kubernetes
  • Operators
  • Helm
  • Platform Engineering

Kubernetes Internals Notes: API Server, RBAC, Scheduling, and Controllers

A practical, student-friendly guide to the Kubernetes request flow, authentication vs authorization, controllers, scheduler behavior, rolling updates, and workload resilience.

  • Kubernetes
  • Platform Engineering
  • AWS

Understanding IRSA in EKS: How Pods Securely Access AWS Resources

A student-friendly guide to IRSA in EKS, explaining why node roles are not enough, how OIDC trust works, and how pods get short-lived AWS credentials safely.

  • AWS
  • Kubernetes
  • Platform Engineering