Chart structure
Chart.yaml, templates, values.
A Helm chart is a package, and like a flat-pack furniture kit it ships with a label on the outside and pre-cut parts inside, laid out the same way every time. Helm (the package manager for Kubernetes, the system that runs your containers across a fleet of machines) reads that label to know what you are building and which version. Inside sit the assembly instructions and a sheet of default measurements you can change before you build. The command helm create hands you a kit already filled with a working example, so you learn the layout by reading it instead of memorizing a spec. The payoff is uniformity: every chart on Artifact Hub (the public hub where Helm charts are published and found) looks familiar once you can read one. For anyone operating or defending a cluster, that tree is also the exact list of what will reach the Kubernetes API server (the control-plane component that accepts and stores every object). Read the chart, and you know what you are about to run.
Scaffold a Chart and Read the Tree
The fastest way to learn the layout is to generate one and look at it. helm create produces a complete, conventional chart built around four things that carry the weight, plus one housekeeping file.
helm create web-apifind web-api | sort
Creating web-apiweb-apiweb-api/.helmignoreweb-api/Chart.yamlweb-api/chartsweb-api/templatesweb-api/templates/NOTES.txtweb-api/templates/_helpers.tplweb-api/templates/deployment.yamlweb-api/templates/hpa.yamlweb-api/templates/httproute.yamlweb-api/templates/ingress.yamlweb-api/templates/service.yamlweb-api/templates/serviceaccount.yamlweb-api/templates/testsweb-api/templates/tests/test-connection.yamlweb-api/values.yaml
Four pieces matter. Chart.yaml is the label. values.yaml holds the default settings. templates/ holds the Kubernetes manifests (YAML descriptions of cluster objects) written as fill-in-the-blank templates: deployment.yaml for the running pods, service.yaml for a stable in-cluster address, hpa.yaml for a HorizontalPodAutoscaler that scales the pod count under load, and ingress.yaml plus httproute.yaml for routing outside traffic in. charts/ is where subcharts (other charts this one depends on) get unpacked. The fifth entry, .helmignore, lists glob patterns (wildcard filename rules such as *.bak) that Helm skips when it loads and packages the chart.
One rule is worth burning into memory. Helm renders every file under templates/ and sends the result to the cluster, with exactly two hard-coded exceptions: files whose names start with an underscore, and NOTES.txt. Everything else, subdirectories included, becomes a Kubernetes object. The underscore file here, _helpers.tpl, holds reusable template snippets that other files pull in; NOTES.txt is the short message printed after an install. You can render the whole chart on your laptop with no cluster attached, using helm template, and that is where every audit should start. The blast radius of a chart is whatever helm template prints, no more and no less.
Chart.yaml, the Label on the Box
Chart.yaml is the label: what this is, which version, and what application it builds. Here is a filled-in one.
apiVersion: v2 # v2 = the modern chart format (Helm 3 and Helm 4)name: web-apidescription: A sample web API servicetype: application # or "library" for a templates-only chartversion: 0.1.0 # the CHART's own version (Semantic Versioning)appVersion: "1.16.0" # the app inside; quote it, it is a stringkeywords:- web- apihome: https://example.com/web-apisources:- https://github.com/example/web-apimaintainers:- name: platform-teamemail: [email protected]# dependencies: # subcharts; see the Subcharts and dependencies lesson# - name: redis# version: "19.x"# repository: https://charts.example.com
apiVersion: v2 marks the modern chart format that Helm 3 and Helm 4 read; v1 is the old Helm 2 format you should never write for new work. The field that trips people up is that there are two versions in this file, and they run on separate clocks. version is the chart's own number, shaped as Semantic Versioning (MAJOR.MINOR.PATCH, so 0.1.0); bump it whenever you change anything in the chart. appVersion is the version of the application inside; it sets the default container image tag and shows up in helm list, and shipping a new build of your app does not force a chart bump unless the templates changed too. For a defender that second field is the fast answer to "which build is running," the first thing you need when a CVE (Common Vulnerabilities and Exposures, a public catalog of known security flaws) lands against your image. type is application for something you deploy, or library for a chart that only exports shared templates and cannot be installed on its own. dependencies declares subcharts, covered in its own lesson.
templates/ and values.yaml, the Blanks and Their Defaults
templates/ and values.yaml are the working pair: the assembly instructions and the sheet of default measurements. The instructions are Go templates (text files with {{ }} placeholders, from the Go programming language) that read from a context, most often .Values (everything in values.yaml) and .Chart (fields from Chart.yaml). values.yaml supplies the defaults those placeholders resolve to, and it doubles as the chart's written contract: it is the first file a consumer opens to see which knobs exist and what they default to.
replicaCount: 1image:repository: nginxpullPolicy: IfNotPresent# Overrides the image tag whose default is the chart appVersion.tag: ""service:type: ClusterIPport: 80resources: {} # left empty so consumers opt in to CPU/memory limits
spec:{{- if not .Values.autoscaling.enabled }}replicas: {{ .Values.replicaCount }}{{- end }}...spec:containers:- name: {{ .Chart.Name }}image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"imagePullPolicy: {{ .Values.image.pullPolicy }}
Read that image line closely. It builds the tag from .Values.image.tag, and when the tag is left empty it falls back to .Chart.AppVersion. values.yaml is also where the security-relevant settings live: the image and its pinned tag, resources limits so one pod cannot starve a node, securityContext, and whether a service account's credentials get mounted. Reading it is how you audit a chart before you trust it. You do not override these by editing the file; consumers layer their own values on top at install time, which the Values and overrides lesson covers. The files prefixed with an underscore, like _helpers.tpl, are meant to be pulled into other templates rather than rendered on their own, and NOTES.txt is a template whose output is printed once the install finishes.
Render Before You Apply
Rendering turns the templates and the values into the plain Kubernetes YAML that would actually be applied. No cluster is touched, so it is safe to run against anything, including a chart you downloaded and do not yet trust.
helm template web-api --show-only templates/deployment.yaml
---# Source: web-api/templates/deployment.yamlapiVersion: apps/v1kind: Deploymentmetadata:name: release-name-web-apilabels:helm.sh/chart: web-api-0.1.0app.kubernetes.io/name: web-apiapp.kubernetes.io/instance: release-nameapp.kubernetes.io/version: "1.16.0"app.kubernetes.io/managed-by: Helmspec:replicas: 1...containers:- name: web-apiimage: "nginx:1.16.0"imagePullPolicy: IfNotPresent
The empty tag became nginx:1.16.0, pulled straight from appVersion, exactly as that template line promised. Notice the labels Helm stamps on everything it renders: app.kubernetes.io/managed-by: Helm and helm.sh/chart: web-api-0.1.0. During an incident those labels let you trace any live object back to the chart and release that created it, instead of guessing who owns a stray Deployment. Rendering first also means you review the real YAML, with every value resolved, before the API server ever sees it.
Keep Junk Out of templates/
The render rule has a sharp edge. Because templates/ renders everything, a file that does not belong there gets treated as a manifest anyway. Watch what a harmless-looking backup does.
# a well-meaning backup, left inside templates/cp web-api/templates/deployment.yaml web-api/templates/deployment-old.yaml# and a .bak copy, which .helmignore *does* listcp web-api/templates/deployment.yaml web-api/templates/deployment.yaml.bak# how many Deployments does the chart render now?helm template web-api | grep -c 'kind: Deployment'
2
Two Deployments, not one. The .bak copy was dropped because .helmignore lists *.bak, but deployment-old.yaml matches nothing in the default ignore list, so Helm rendered it into a second, real Deployment. On a live cluster that is a duplicate object you never meant to create. Swap that backup for a stray README.md and you get the loud version instead: Error: YAML parse error on web-api/templates/README.md: error unmarshaling JSON: ... cannot unmarshal string into Go value of type util.SimpleHead. Either way, the fix is the same: keep the directory clean and render before you ship.
helm template web-api | grep -c 'kind: Deployment'. What number prints, and why?tag: "" in values.yaml, and the Deployment reads image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}". You edit Chart.yaml to appVersion: "1.17.0", leave version: 0.1.0 untouched, and rerun helm template. What image line renders?type: application to type: library in Chart.yaml so other charts can pull in the helpers, then runs helm install mylib ./mylib. What should you tell them to expect?Before every apply, run helm template and read the output like a diff. The file you forgot to delete is the object you did not mean to create, and the render is the only place you will catch it before the API server does.
Try this
Run helm create web-api on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: everything in templates/ is a manifest, including your mistakes. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.