TT Lab
Get started
Learn Learning paths Courses

Authoring and Shipping Helm Charts

A Chart Is a Package, Not a Bundle of Manifests

Continue in TT Lab

Summary in one line

A chart is not a folder that collects YAML; it is a package that carries a version, default values, and usage together.

Why this is needed

At first, a single kubectl apply -f deployment.yaml is enough. But the moment you have three environments — development, staging, and production — the file splits into three copies. At first only the replica count differs, but after half a year the three files have become different creatures. An annotation that exists only in production, an old image tag left only in development, a resource limit that nobody knows which side is right about. The real problem here is not that there are three copies of the file, but that which of these values are allowed to change is written nowhere.

A chart answers that question with structure. Values that may be changed go in values.yaml, the shape that must not change goes in templates/, and what this bundle is and which revision it is go in Chart.yaml. The very fact that the three places have different roles becomes documentation. A newcomer who opens only values.yaml sees every "knob I can touch."

How it works

Each place in a chart directory has a fixed meaning.

Place What it is
Chart.yaml The chart's ID card. Name, version, appVersion, dependencies
values.yaml The only interface users read and edit. Default values
templates/ Files that are rendered into manifests
templates/_helpers.tpl Starts with an underscore — holds not manifests but only named template definitions
templates/NOTES.txt A guide a person reads right after installation
charts/ The place where dependency charts (subcharts) go
crds/ A special zone applied only at install and not touched during upgrade or uninstall
.helmignore Things to leave out of packaging

What people often confuse in Chart.yaml is the two versions. version is the SemVer of the chart itself, and you raise it when you change the templates or the structure of the defaults. appVersion is the version of the application the chart deploys, and it flows into the default image tag and the app.kubernetes.io/version label. The two move independently — it is perfectly normal for only the chart version to go up because you fixed one label while the app stays the same. apiVersion must be v2 in Helm 3, and type is either application, which creates actual resources, or library, which provides only named templates.

Kubernetes has an official standard for labels. app.kubernetes.io/name, instance, version, managed-by, and also helm.sh/chart. If you write these labels out by hand over and over, they are bound to drift apart, so gather them in one place in _helpers.tpl. An important design point comes out of this. You must separate the common labels from the selector labels. A Deployment's spec.selector is a field that cannot be changed after creation, but the common labels have the chart version and the app version mixed in. If you use them as they are in the selector, the moment you raise the chart version the selector changes, and the upgrade is rejected by the API server. So put only the two that never change (name, instance) in the selector.

What it looks like in the field

First, the 63-character wall. A resource name is usually made by joining the release name and the chart name. When your team starts using release names like payments-api-canary-eu-west, one day the name suddenly exceeds the rule and installation fails. That is why name helpers conventionally carry trunc 63 and trimSuffix "-". After truncation, ending with a hyphen is also invalid.

Second, a values.yaml with no comments. What someone who uses a chart reads is not the templates but values.yaml alone. If there are no comments there, the user has to dig through the templates to find out "what happens if I change this value." Naming values well and adding comments lasts far longer than writing separate documentation.

Third, lint is not a syntax checker. helm lint also checks whether the YAML is broken, but in fact it looks more at whether conventions were followed. Most of its warnings say things like the icon is missing or a recommended label is missing. If the habit of ignoring warnings takes hold, real errors get buried among them.

What you need in place when giving a chart to others

While only your own team uses a chart, it runs even if you build it carelessly, but once others start using it, they come to depend on things you never promised, and you can no longer change it. If you decide a few things at the start, that problem shrinks.

values.yaml is the documentation. If you write a default value for every key, the user knows from a single file what they can change. Write units and allowed values in comments. If you leave out keys that may be absent altogether, the user does not even know such keys exist.

Block wrong values in advance with values.schema.json. If a type is wrong or a required key is missing, it fails before rendering, so nothing strange gets put on the cluster.

The two versions in Chart.yaml are different. version is the version of the chart itself and appVersion is the version of the application it contains. If you changed only the chart, raise only the former. If you move the two together, you can no longer tell a chart fix from an app deployment.

Create names in one helper. If every resource uses the fullname in _helpers.tpl, resource names follow consistently even when the release name changes. If you write names directly in each resource, somewhere they are bound to drift.

Follow the standard for labels. If you attach app.kubernetes.io/name, instance, version, component, and managed-by, other tools recognize them. And never change the labels used in the selector — a Deployment's selector is immutable, so if you change it, the upgrade fails and you have to delete and recreate.

Write the next action in NOTES.txt. It is the only guidance shown on screen after installation. How to connect, a command to check, and one common mistake are enough.

Pin dependencies with Chart.lock. If you write a range in dependencies and do not commit the lock file, the same chart pulls in different subcharts than yesterday.

What you will do in the next lab

You create a chart skeleton at /root/helm/lab/labhub-web and fill in Chart.yaml and values.yaml yourself. You pull the name and labels out into helpers so that standard labels are attached to every object, and render and save the installation notes. At the end you place the rendering with overridden values and the default rendering side by side and check that the Service selector and the Pod labels really match.