Understanding Chart Structure
Explore the anatomy of a Helm chart. Understand Chart.yaml, templates, values, and how all the pieces fit together.
Understanding Chart Structure
In the previous tutorial, we built a minimal chart with just a few files. That works for learning, but real-world charts have more structure. Let's understand every piece of a Helm chart so nothing feels like magic.
The Full Chart Layout
Here's what a complete, production-ready chart looks like:
my-chart/
├── Chart.yaml # Chart metadata (required)
├── Chart.lock # Locked dependency versions
├── values.yaml # Default configuration values
├── values.schema.json # JSON Schema for values validation
├── .helmignore # Files to exclude from packaging
├── LICENSE # Chart license
├── README.md # Chart documentation
├── charts/ # Dependency charts
│ └── redis-17.0.0.tgz
├── crds/ # Custom Resource Definitions
│ └── my-crd.yaml
└── templates/ # Kubernetes manifest templates
├── NOTES.txt # Post-install usage notes
├── _helpers.tpl # Named template definitions
├── deployment.yaml
├── service.yaml
├── configmap.yaml
├── secret.yaml
├── ingress.yaml
├── serviceaccount.yaml
├── hpa.yaml
└── tests/
└── test-connection.yaml
"Do I need all of these?"
Nope. Only Chart.yaml is truly required, and you'll need at least one template to be useful. Everything else is optional. Let's go through each piece.
Chart.yaml — The Identity Card
This is the only required file. It describes what your chart is:
apiVersion: v2
name: my-app
description: A web application with Redis caching
type: application
version: 1.2.0
appVersion: "3.4.1"
# Optional but recommended
keywords:
- web
- redis
- caching
home: https://github.com/your-org/my-app
sources:
- https://github.com/your-org/my-app
maintainers:
- name: Your Name
email: you@example.com
url: https://your-site.com
icon: https://your-cdn.com/my-app-icon.png
# Dependencies (we'll cover these in the dependencies tutorial)
dependencies:
- name: redis
version: "17.x.x"
repository: https://charts.bitnami.com/bitnami
Key Fields
| Field | Required | Description |
|---|---|---|
apiVersion | Yes | Always v2 for Helm 3 |
name | Yes | Chart name (lowercase, hyphens OK) |
version | Yes | Chart version (SemVer) |
type | No | application (default) or library |
appVersion | No | Version of the app being deployed |
description | No | One-line description |
dependencies | No | Required sub-charts |
"What's a library chart?"
A library chart provides only named templates (helpers) — no deployable resources. Other charts import it as a dependency to reuse common template logic. Think of it like a utility package. We'll cover this more in the named templates tutorial.
values.yaml — Configuration Central
This file defines every configurable parameter and its default value:
# values.yaml
replicaCount: 1
image:
repository: my-app
tag: "latest"
pullPolicy: IfNotPresent
nameOverride: ""
fullnameOverride: ""
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
hosts:
- host: my-app.local
paths:
- path: /
pathType: Prefix
resources:
limits:
cpu: 100m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
autoscaling:
enabled: false
minReplicas: 1
maxReplicas: 10
targetCPUUtilization: 80
A few conventions:
- Use nested keys —
image.repositoryis cleaner thanimageRepository - Provide sensible defaults — the chart should work with zero overrides
- Use
enabled: falsefor optional features like ingress, autoscaling, etc. - Comment your values — future you (and your teammates) will thank you
Values Hierarchy
Remember, values can be overridden at multiple levels:
# 1. Defaults from values.yaml (lowest priority)
helm install my-app ./my-chart
# 2. Override with a file
helm install my-app ./my-chart -f production.yaml
# 3. Override with --set (highest priority)
helm install my-app ./my-chart -f production.yaml --set replicaCount=10
templates/ — Where the Magic Happens
This directory contains Go template files that Helm renders into Kubernetes manifests. Every .yaml file here gets processed through the template engine.
Template File Naming
There's no strict naming rule, but conventions help:
templates/
├── deployment.yaml # One file per resource type
├── service.yaml
├── configmap.yaml
├── ingress.yaml # Conditional (only if ingress.enabled)
├── hpa.yaml # Conditional (only if autoscaling.enabled)
├── _helpers.tpl # Underscore prefix = not rendered as manifest
├── NOTES.txt # Special: printed after install
└── tests/
└── test-connection.yaml
Important rules:
- Files starting with
_(like_helpers.tpl) are not rendered as Kubernetes manifests. They contain helper templates. NOTES.txtis special — it's rendered and printed to the user after install/upgrade, but not sent to Kubernetes.- Files in
tests/are used byhelm test— we'll cover that later.
Built-in Template Objects
Every template has access to these objects:
# Release info
{{ .Release.Name }} # Release name (helm install NAME)
{{ .Release.Namespace }} # Target namespace
{{ .Release.Revision }} # Revision number (increments on upgrade)
{{ .Release.IsUpgrade }} # true if this is an upgrade
{{ .Release.IsInstall }} # true if this is a fresh install
# Chart info
{{ .Chart.Name }} # From Chart.yaml
{{ .Chart.Version }} # From Chart.yaml
{{ .Chart.AppVersion }} # From Chart.yaml
# Values
{{ .Values.replicaCount }} # From values.yaml or overrides
# Kubernetes cluster info
{{ .Capabilities.KubeVersion }} # Cluster K8s version
{{ .Capabilities.APIVersions }} # Available API versions
# Template info
{{ .Template.Name }} # Current template file path
{{ .Template.BasePath }} # templates/ directory path
A Real-World Template Example
# templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "my-chart.fullname" . }}
labels:
{{- include "my-chart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "my-chart.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "my-chart.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: 80
{{- if .Values.resources }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
{{- end }}
Don't panic if this looks complex. We'll break down templating syntax, functions, and control flow in the next few tutorials.
_helpers.tpl — Reusable Template Snippets
This file defines named templates that other templates can call. It's like a utility library for your chart:
{{- define "my-chart.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- define "my-chart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name .Chart.Name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "my-chart.labels" -}}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "my-chart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "my-chart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
"Why 63 characters?"
Kubernetes resource names can't exceed 63 characters (DNS label limit). The trunc 63 ensures names stay within bounds even when release names are long.
NOTES.txt — Post-Install Instructions
This template runs after install/upgrade and prints helpful info:
Thank you for installing {{ .Chart.Name }}!
Your release "{{ .Release.Name }}" has been deployed to namespace "{{ .Release.Namespace }}".
To access the application:
{{- if eq .Values.service.type "NodePort" }}
export NODE_PORT=$(kubectl get svc {{ include "my-chart.fullname" . }} -o jsonpath='{.spec.ports[0].nodePort}')
echo "Visit http://localhost:$NODE_PORT"
{{- else if eq .Values.service.type "LoadBalancer" }}
NOTE: It may take a few minutes for the LoadBalancer IP to be available.
kubectl get svc {{ include "my-chart.fullname" . }} -w
{{- else }}
kubectl port-forward svc/{{ include "my-chart.fullname" . }} 8080:{{ .Values.service.port }}
echo "Visit http://localhost:8080"
{{- end }}
.helmignore — Excluding Files
Like .gitignore but for Helm packaging. Files listed here won't be included when you helm package:
# .helmignore
.git
.gitignore
*.swp
*.bak
*.tmp
.DS_Store
ci/
*.md
!README.md
charts/ — Dependencies
This directory holds dependency charts (as .tgz archives). When you declare dependencies in Chart.yaml and run helm dependency update, they're downloaded here.
helm dependency update ./my-chart
charts/
├── redis-17.0.0.tgz
└── postgresql-12.1.0.tgz
We'll cover dependencies in detail in a later tutorial.
crds/ — Custom Resource Definitions
If your chart needs Custom Resource Definitions, put them in crds/. Helm installs CRDs before rendering templates, ensuring the CRD types exist when your resources reference them.
# crds/my-crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: myresources.example.com
spec:
group: example.com
names:
kind: MyResource
plural: myresources
scope: Namespaced
versions:
- name: v1
served: true
storage: true
Important: Helm will never delete or upgrade CRDs. This is by design — CRDs are cluster-wide and deleting them would destroy all custom resources. You have to manage CRD upgrades manually.
values.schema.json — Validating Values
Want to prevent users from passing garbage values? Define a JSON Schema:
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["replicaCount", "image"],
"properties": {
"replicaCount": {
"type": "integer",
"minimum": 1
},
"image": {
"type": "object",
"required": ["repository"],
"properties": {
"repository": {
"type": "string"
},
"tag": {
"type": "string"
}
}
}
}
}
Now if someone tries --set replicaCount=banana, Helm will reject it before anything touches the cluster.
What's Next?
You now understand every piece of a Helm chart. Here's the recap:
Chart.yaml— metadata and dependenciesvalues.yaml— configurable defaultstemplates/— Go templates rendered into K8s manifests_helpers.tpl— reusable template snippetsNOTES.txt— post-install user instructions- Built-in objects:
.Release,.Values,.Chart,.Capabilities
In the next tutorial, we'll dive deep into values and templating — the Go template syntax that makes Helm charts dynamic. This is where it gets really fun. Let's go!