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:
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.mdAt 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:
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.yamlThis 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:
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-configLooks 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:
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:
kubectl apply -f deployment.yamlkubectl apply -f service.yamlkubectl apply -f ingress.yamlkubectl apply -f configmap.yamlkubectl apply -f secret.yamlWith Helm:
helm install my-app ./chartThat 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:
mychart/├── Chart.yaml├── values.yaml├── templates/│ ├── deployment.yaml│ ├── service.yaml│ ├── ingress.yaml│ └── configmap.yamlLet us break this down.
Chart.yaml - chart metadata#
This file describes the chart itself.
Example:
apiVersion: v2name: my-appdescription: Helm chart for my applicationtype: applicationversion: 0.1.0appVersion: "1.0.0"What this means:
name: chart nameversion: chart versionappVersion: 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:
replicaCount: 2 image: repository: myrepo/backend tag: "v1" pullPolicy: IfNotPresent service: type: ClusterIP port: 80 containerPort: 8080 ingress: enabled: true host: myapp.example.comThis 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:
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:
my-project/├── helm/│ └── my-app/│ ├── Chart.yaml│ ├── values.yaml│ └── templates/├── values/│ ├── dev.yaml│ ├── staging.yaml│ └── prod.yamlThis 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:
replicaCount: 1 image: tag: "dev-42" ingress: host: dev.myapp.example.comAnd values/prod.yaml might look like this:
replicaCount: 5 image: tag: "v1.3.2" ingress: host: myapp.example.comNow the same chart can behave differently depending on which file you use.
Install in dev:
helm install my-app ./helm/my-app -f values/dev.yamlInstall in prod:
helm install my-app ./helm/my-app -f values/prod.yamlSame 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:
helm install my-app ./helm/my-app -f values/prod.yamlIn this command:
my-appis the release name./helm/my-appis the chartvalues/prod.yamlcustomizes 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.
List releases:
helm listCheck release history:
helm history my-appExample output:
REVISION STATUS CHART APP VERSION1 deployed my-app-0.1.0 1.0.02 deployed my-app-0.2.0 1.1.0This 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:
helm upgrade my-app ./helm/my-app -f values/prod.yamlWhat Helm does:
- loads the chart
- loads the values file
- renders the final Kubernetes manifests
- compares them with what is already deployed
- 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:
helm history my-appRoll back to revision 1:
helm rollback my-app 1That 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:
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.mdWhy 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:
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.mdWhy 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:
helm repo add grafana https://grafana.github.io/helm-chartshelm repo updateThen install Grafana:
helm install my-grafana grafana/grafanaThat 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:
helm repo add prometheus-community https://prometheus-community.github.io/helm-chartsNow Helm knows where to find charts published there.
You can then search or install charts from that source.
Search repositories:
helm search repo prometheusThat 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:
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: NeverWhere does this file go?
Inside the chart's templates/ directory, usually as its own file.
For example:
helm/my-app/├── Chart.yaml├── values.yaml└── templates/ ├── deployment.yaml ├── service.yaml ├── ingress.yaml └── pre-install-job.yamlWhy 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:
apiVersion: v2name: my-platformversion: 0.1.0 dependencies: - name: grafana version: "8.x.x" repository: "https://grafana.github.io/helm-charts"Then fetch dependencies:
helm dependency update ./helm/my-platformThis 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.
helm lint ./helm/my-appRender templates locally#
This shows the final Kubernetes YAML Helm would send to the cluster.
helm template my-app ./helm/my-app -f values/dev.yamlThis 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:
- build container image
- push image to registry
- deploy using Helm with the right values file
Example:
# 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.2Why 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:
- create a very small app chart
- add a Deployment and Service template
- create
dev.yamlandprod.yaml - install with one values file
- upgrade using a new image tag
- check release history
- roll back to the previous revision
- 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#
helm repo add grafana https://grafana.github.io/helm-chartshelm repo updateSearch for charts#
helm search repo grafanaInstall a chart#
helm install my-app ./helm/my-app -f values/dev.yamlUpgrade a release#
helm upgrade my-app ./helm/my-app -f values/prod.yamlUpgrade or install if missing#
helm upgrade --install my-app ./helm/my-app -f values/prod.yamlView releases#
helm listView release history#
helm history my-appRoll back#
helm rollback my-app 1Lint chart#
helm lint ./helm/my-appRender manifests locally#
helm template my-app ./helm/my-app -f values/dev.yamlFetch dependencies#
helm dependency update ./helm/my-app