24.1 Chart Directory Structure: Chart.yaml, values.yaml, templates/
Right, let’s get our hands dirty. You’re about to build a Helm chart, which is essentially a fancy package for your Kubernetes manifests. The structure isn’t just a formality; it’s the entire skeleton of your application deployment. Get this wrong, and you’ll be fighting your own creation. Get it right, and you have a powerful, reusable artifact.
Think of a Helm chart directory as a house with three crucial rooms: the legal deed (Chart.yaml), the interior design plans (values.yaml), and the raw, buildable framework (templates/). You can’t have one without the others.
The Meta File: Chart.yaml
This is your chart’s birth certificate and resume, all in one. It’s pure metadata. Helm uses this to understand what it’s dealing with, what your chart depends on, and who to blame if it breaks. The apiVersion field is the most important one to get right; it dictates which features of Helm you can use. For modern charts, you want v2, but not the old Helm v2 client version—that’s confusing, I know. Let me explain.
Helm v3 introduced two apiVersions: v1 (for backward compatibility with Helm v2 charts) and v2 (for the new, cleaner Helm v3 chart structure). You almost always want v2 because v1 charts require a requirements.yaml file for dependencies, which is a mess we’re happy to leave in the past.
apiVersion: v2
name: my-brilliant-app
description: A chart for deploying my frankly brilliant application.
type: application
version: 0.1.0
appVersion: "2.1.3"
dependencies:
- name: redis
version: "16.13.0"
repository: "https://charts.bitnami.com/bitnami"
condition: redis.enabled
See that dependencies block? That’s the new, correct way to do it. It declares that for this chart to work, it might need a Redis cluster. The condition line is your escape hatch; it says, “Only install this if the user sets redis.enabled to true in their values.yaml.” This is how you keep your chart modular and avoid deploying a database every single time.
The User’s Control Panel: values.yaml
This file is your contract with the user. It’s where you define every knob, dial, and lever they can tweak without having to rip apart your templates. Your goal here is to provide sensible defaults for everything. I cannot stress this enough: Assume the person using your chart is in a hurry and will not read your documentation. If your app needs a database password, provide a default, even if it’s insecure! Why? Because it will make the chart “just work” for a quick test, and it forces the user to consciously change it for production, which is a much clearer signal than a chart that fails mysteriously.
Organize this file logically. Group database settings together, service settings together, and so on. Commentary is your friend. Explain what things do and, more importantly, what the valid values are.
# -- Enable persistence using a Persistent Volume Claim
persistence:
enabled: true
# -- The PVC storage class. If undefined, uses the default StorageClass.
storageClass: ""
size: 8Gi
# -- Configuration for the application container
image:
repository: nginx
tag: "1.25"
pullPolicy: IfNotPresent
# -- SMTP configuration for notifications
smtp:
host: ""
port: 587
user: ""
# -- Don't put passwords here, you maniac! Use a --set flag or a secrets file.
password: ""
Notice the -- comments? Helm can actually use these to generate documentation. You’re writing docs and code simultaneously. Efficiency!
The Magic Trick: templates/ directory
This is where the wizard lives. This directory contains all your Kubernetes YAML manifests, but they’re written in Go template language. Helm will take these templates, combine them with the values from values.yaml (or user overrides), and render plain, valid YAML that it sends to Kubernetes.
The most important file here is _helpers.tpl. This is your utility belt. You define named templates here for things you repeat constantly, like your chart’s labels. This is the single best way to avoid the nightmare of updating the same name in seventeen different files.
{{/*
Common labels
*/}}
{{- define "my-brilliant-app.labels" -}}
helm.sh/chart: {{ include "my-brilliant-app.chart" . }}
app.kubernetes.io/name: {{ include "my-brilliant-app.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
And then in your deployment.yaml, you use it cleanly:
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-brilliant-app.fullname" . }}
labels:
{{- include "my-brilliant-app.labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "my-brilliant-app.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-brilliant-app.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
The | nindent 4 is your best friend for keeping the generated YAML readable and correctly indented. The biggest pitfall here is over-templating. Don’t create a variable for every single string. If it’s not something a user needs to change, hardcode it in the template. Your future self, trying to debug this at 2 a.m., will thank you for the simplicity.