Dissecting how Helm 3 works from the inside: client-only architecture without Tiller, release storage in Secrets, the Chart, Repository, Release, Values and Templates concepts, the chart directory structure, and the three-way strategic merge patch mechanism for upgrade and rollback.

After episode 1, where we understood why Helm exists — answering the problem of managing dozens of YAML files, environment-specific configuration, and application lifecycle — in this episode we open up the hood. We'll dissect the architecture and core concepts of Helm 3: how it works without Tiller, where release state is stored, what concepts form Helm's everyday vocabulary, what a well-structured chart looks like, and — the part most rarely explained clearly — what actually happens behind upgrade and rollback.
Why is it important to understand the architecture, not just type commands? Because almost every "mysterious" Helm error is rooted in an architecture misunderstanding: people who don't know releases are stored as Secrets will be confused by the strange Secret named sh.helm.release.v1.* in their namespace; people who don't understand three-way merge will wrongly assume that --force resolves all conflicts. Like taking apart a car engine, understanding the parts means you can not only drive but also fix things when they stall.
The biggest architectural difference between Helm 3 and its predecessor is that there is no server component. Helm 3 is a single client binary (helm) that runs on your machine — no daemon, no service that needs to be deployed to the cluster.
When you run helm install, the Helm client reads the same kubeconfig file as kubectl, obtains the credentials relevant to the active context, then communicates directly with the Kubernetes API server over HTTPS. No intermediary. The important consequence: all access control follows standard Kubernetes RBAC. If your user only has access to the dev namespace, then the releases you install can only touch the dev namespace — not the entire cluster as was the case with Tiller in Helm 2.
helm CLI (client lokal)
│ kubeconfig (RBAC user)
▼
Kubernetes API server
├── installs manifests (Deployment, Service, ...)
└── stores release state (Secret, namespace-scoped)Tip
Because Helm uses kubeconfig, you can switch targets simply by changing the context: kubectl config use-context dev-cluster and then helm install ... will automatically deploy to the dev cluster. There is no separate Helm configuration to sync — one source of truth for credentials.
Helm 3 stores all state of each release as a Secret of type helm.sh/release.v1, in the namespace where the release is installed (not in kube-system as in the Tiller era). Each installation produces a new Secret; each upgrade produces another new Secret. Here's how to see it:
kubectl get secrets
kubectl get secret sh.helm.release.v1.myapp.v1 -o yamlapiVersion: v1
kind: Secret
metadata:
name: sh.helm.release.v1.myapp.v1
namespace: myapp
type: helm.sh/release.v1
data:
release: eyJjaGFydCI6Ik15YXBwIiwi...The release field is the rendered manifest of the entire application, stored in base64-compressed form. This is where helm get manifest, helm rollback, and helm history read their data. One important practical implication: deleting the secret means losing the release history. Never manually delete a sh.helm.release.* Secret if you still want Helm to manage that release.
There are two more implications worth noting. First, because it's stored as a Secret, its contents are encrypted at rest in etcd (if your cluster enables etcd encryption) — manifest data that may contain sensitive configuration isn't readable in plain text. Second, because it's namespace-scoped, one release can't accidentally see or affect another release in a different namespace — isolation that aligns with the standard namespace RBAC model.
These five concepts are Helm's required vocabulary. Master them, because every command and every discussion in this series is built on them.
A Chart is an application package: a directory containing metadata (Chart.yaml), default configuration (values.yaml), and a set of templates (templates/) that produce Kubernetes manifests. A chart can be a directory, a .tgz archive, or a reference to a repository. One chart describes one complete application — Deployment + Service + Ingress + ConfigMap in a single package.
A Repository is a chart storage location, accessed over HTTP/HTTPS or an OCI registry. Repositories store .tgz archives and an index.yaml file that maps chart names to available versions. The analogy is exactly the npm registry: you add the registry, then pull packages from it.
A Release is one instance of a chart installed into the cluster under a unique name. This is the crucial point that distinguishes Helm from mere templating: helm install myapp nginx produces a release named myapp, and Helm tracks its entire history — every change produces a new revision that can be rolled back.
Values are the chart's configuration: the values injected into templates during rendering. Every chart has a values.yaml with defaults; users can override via files (-f) or command-line parameters (--set). These values are what let one chart serve many environments without changing the template at all.
These values have a strict hierarchy: values.yaml (defaults) is the bottom layer, override files (-f values-prod.yaml) layer on top of it, and --set on the command line overrides both. This hierarchy is what lets one chart serve dev, staging, and production simply by swapping values files — a topic we'll cover thoroughly in episode 6.
Templates are manifest files written in Go templates with syntax like {{ .Values.replicaCount }}. On install, Helm renders every template with the effective values, producing the final YAML sent to the API server. Templates are the "code," values are the "data" — that separation is what makes charts reusable.
Templates don't just replace placeholders: they can include files, call helpers, loop, and conditionally adjust the output structure — for example, "create an Ingress only if ingress.enabled is true." You'll start building your own templates in episode 7 and dig into the templating language through episode 10.
A healthy chart has an almost identical structure every time. Let's break it down one by one:
my-chart/
├── Chart.yaml # chart metadata: name, version, apiVersion, dependencies
├── values.yaml # default configuration values
├── .helmignore # files ignored during packaging (gitignore patterns)
├── README.md # chart usage documentation
├── LICENSE # distribution license (required for public charts)
├── charts/ # packaged dependencies (subcharts)
├── crds/ # CustomResourceDefinition (installed globally, once only)
└── templates/ # manifest templates + helpers + NOTES
├── _helpers.tpl # named templates that can be included
├── deployment.yaml
├── service.yaml
├── NOTES.txt # message shown after install
└── NOTES.mdapiVersion, name, version, appVersion, description, type, and the dependencies list..yaml/.tpl file here is rendered (except those starting with _).helm dependency (covered in episode 11).An example of a healthy Chart.yaml — note apiVersion: v2, version (package version), and appVersion (application version):
apiVersion: v2
name: my-chart
description: A Helm chart for my application
type: application
version: 0.1.0
appVersion: "1.16.0"
dependencies:
- name: postgresql
version: "15.x.x"
repository: https://charts.bitnami.com/bitnamiThe dependencies field is what lets one chart pull in other charts automatically on install — and their versions will be locked in Chart.lock. You'll build charts like this from scratch in episode 7, and dive into dependencies in episode 11.
The internal flow of helm install — and helm upgrade too — is a four-stage pipeline:
values.schema.json schema if present), then asks the API server whether the resources can be created (server-side dry run).Because of this order, a failure at any stage leaves a trace you can inspect: --dry-run to test the render, kubectl get secrets to see whether the release was saved, and kubectl get pods to see whether the resources are actually alive.
The render stage can also be run without a cluster at all with helm template — the fastest way to inspect a chart's output without touching the API server:
helm template web bitnami/nginx --namespace web
helm template web bitnami/nginx --set replicaCount=5 | head -40The first command produces all the manifests that would be sent; the second shows them with the replicaCount=5 override applied. This is Helm's main transparency window: whatever is written in these files is exactly what will be sent to the API server.
Here's where the real magic happens. When you run helm upgrade, Helm doesn't simply "send new YAML." It merges three sources:
From these three, Helm computes a strategic merge patch that only changes the parts that actually differ — and crucially, doesn't undo changes made by others to resources the chart doesn't touch. That's what makes a Helm upgrade "safe": you change replicas from 3 to 5, and label changes made by another tool on that resource don't get wiped out.
The easiest analogy: imagine three copies of a document — the old copy (previous revision), the new copy (what you want to apply), and the copy on the wall (the actual cluster state). Helm compares all three field by field: if replicas changed in the new copy, it updates the value; if an annotation was added by someone else to the wall copy and isn't touched by either copy, it leaves it in place. The result of that comparison is the strategic merge patch sent to the API server — not overwriting the whole file like an ordinary diff.
All the differences above are summarized in the following table:
| Aspect | Helm 2 | Helm 3 |
|---|---|---|
| Architecture | Client + Tiller (daemon in cluster) | Client-only, straight to the API server |
| Security model | Tiller had broad access, hard to RBAC | Standard Kubernetes RBAC (kubeconfig) |
| Release storage | ConfigMap in the kube-system namespace | Secret in the release namespace |
| Chart schema | apiVersion: v1 | apiVersion: v2 |
| Values validation | None | JSON Schema (values.schema.json) |
| Dependencies | No lockfile | Chart.lock for reproducibility |
| OCI registry | Not supported | Supported (Helm 3.8+) |
The last three points deserve emphasis. apiVersion: v2 marks the modern chart schema that adds declarative dependencies, type, and appVersion. JSON Schema validation means a chart can define a strict contract for input values — data type errors are caught at install time, not when the application explodes at runtime (covered in depth in episode 13). Chart.lock locks dependency versions, so helm dependency build produces an identical chart on every machine — the foundation of reproducibility and a secure supply chain.
Important
If you come across an old chart with apiVersion: v1 in its Chart.yaml, Helm 3 can still install it (backward compatibility), but all v2 features — declarative dependencies, type, schema validation — are unavailable. This entire series uses apiVersion: v2 charts.
In episode 2 we've dissected the Helm 3 architecture from the inside: client-only without Tiller, communicating directly with the API server using standard RBAC, release state stored as Secrets in the release namespace, five core concepts (Chart, Repository, Release, Values, Templates), the standard chart directory structure, and the four-stage workflow render → validate → apply → save with the three-way strategic merge patch mechanism behind upgrade and rollback.
Key takeaways:
sh.helm.release.* Secret — don't delete it manually.apiVersion: v2, JSON Schema, and Chart.lock.Now you understand the engine behind Helm. In the next episode, episode 3, we climb into the cockpit: Helm installation and CLI basics — from the installer script, shell completion, command structure, repository management, finding charts on Artifact Hub, to your first deployment using Helm. See you in episode 3!