Skip to content
7 min read·Lesson 2 of 8

Anatomy of a Helm Chart

Every file in a Helm chart explained: Chart.yaml, values.yaml, templates, _helpers.tpl, NOTES.txt, charts/, and crds/.

A Helm chart is just a directory. Helm cares about the names and locations of files inside it — there is no central registration. helm create my-app scaffolds the canonical layout.

The Canonical Layout

my-app/
├── Chart.yaml          # Chart metadata
├── values.yaml         # Default configuration
├── values.schema.json  # Optional JSON schema validating values
├── README.md           # Human-readable documentation
├── LICENSE             # Optional licence file
├── .helmignore         # Files to exclude when packaging
├── charts/             # Sub-charts (dependencies)
├── crds/               # CustomResourceDefinitions (installed once)
└── templates/
    ├── NOTES.txt       # Printed after install/upgrade
    ├── _helpers.tpl    # Reusable template partials
    ├── deployment.yaml
    ├── service.yaml
    ├── ingress.yaml
    ├── serviceaccount.yaml
    ├── hpa.yaml
    └── tests/
        └── test-connection.yaml

Chart.yaml

The identity card of the chart. The minimum is name, version, apiVersion. Modern charts use apiVersion: v2 which adds dependencies, type, and kubeVersion support.

apiVersion: v2
name: my-app
description: My production web application
type: application       # or 'library' (no resources, only helpers)
version: 1.4.2          # Chart version — bump on every change
appVersion: "2.7.0"     # Application version — string, quoted
kubeVersion: ">=1.27"   # Cluster version constraint
icon: https://example.com/icon.png
keywords: [web, api, microservice]
home: https://example.com
sources: [https://github.com/acme/my-app]
maintainers:
  - name: Platform Team
    email: platform@acme.io
dependencies:
  - name: postgresql
    version: "12.x.x"
    repository: "oci://registry-1.docker.io/bitnamicharts"
    condition: postgresql.enabled

Crucial distinction:

  • version — the chart's own SemVer. Bumped every time the chart changes, even for typo fixes.
  • appVersion — the version of the software the chart deploys. Doesn't have to be SemVer; quote it.

values.yaml

The default configuration. Users override at install time via -f my-values.yaml or --set key=value. Keep the file small, well-commented, and grouped:

replicaCount: 2

image:
  repository: ghcr.io/acme/my-app
  pullPolicy: IfNotPresent
  tag: ""              # Empty = falls back to .Chart.AppVersion

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  className: nginx
  hosts:
    - host: chart-example.local
      paths: ["/"]

resources:
  limits:   { cpu: 500m, memory: 512Mi }
  requests: { cpu: 100m, memory: 128Mi }

autoscaling:
  enabled: false
  minReplicas: 2
  maxReplicas: 10

values.schema.json

Optional, but strongly recommended for charts other people will consume. Helm validates the merged values against the schema before rendering — fail fast on typos and bad types:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "required": ["image"],
  "properties": {
    "replicaCount": { "type": "integer", "minimum": 1 },
    "image": {
      "type": "object",
      "required": ["repository"],
      "properties": {
        "repository": { "type": "string" },
        "tag": { "type": "string" }
      }
    }
  }
}

templates/

Every .yaml file here is rendered through the Go template engine and the output applied to the cluster. Special files:

  • NOTES.txt — text rendered and printed at the end of helm install. Use it to show URLs, next-step commands, secrets to fetch.
  • Files starting with _ (e.g., _helpers.tpl) — partials only. Not rendered as resources. Used to define reusable template blocks called via include.
  • templates/tests/ — manifests with the annotation "helm.sh/hook": test. Run via helm test <release>.

charts/

Holds sub-charts (dependencies). Either committed by hand (vendored) or pulled by helm dependency update from the registries listed in Chart.yaml. We will cover dependencies in lesson 5.

crds/

A special directory: anything inside crds/ is installed once on first install and never modified on upgrade or delete. This protects CRDs from being accidentally removed (which would cascade-delete every custom resource of that type). For evolving CRDs use a separate "CRDs chart" and accept the operational responsibility.

.helmignore

Like .gitignore but for helm package. Patterns matched here are excluded from the .tgz. The default ignores Git artefacts, IDE files, and OS noise. Add anything large or sensitive (test data, design docs).

Built-in Objects

Inside templates Helm exposes a small set of top-level objects. Memorise these — they are the API:

ObjectContents
.ValuesMerged values (defaults + overrides)
.ChartFields from Chart.yaml
.ReleaseRelease-level info: Name, Namespace, IsInstall, IsUpgrade, Revision
.FilesAccess to non-template files in the chart (configs, scripts)
.CapabilitiesCluster info: KubeVersion, APIVersions
.TemplateInfo about the current template file

In the next lesson we put these to work.

Key Takeaways

  • A chart is a directory with a fixed layout — Helm rejects anything that doesn't match.
  • Chart.yaml declares identity, version, and dependencies; values.yaml supplies defaults.
  • Files starting with an underscore (_helpers.tpl) are partials, not rendered as resources.
  • CRDs live in crds/ and are installed once, never upgraded by Helm itself.
  • .helmignore controls what is packaged into the chart archive.

Test your knowledge

Try exam-style practice questions to reinforce what you've learned.

Practice Questions →