Understanding Chart Structure

Explore the anatomy of a Helm chart. Understand Chart.yaml, templates, values, and how all the pieces fit together.

8 min read

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

FieldRequiredDescription
apiVersionYesAlways v2 for Helm 3
nameYesChart name (lowercase, hyphens OK)
versionYesChart version (SemVer)
typeNoapplication (default) or library
appVersionNoVersion of the app being deployed
descriptionNoOne-line description
dependenciesNoRequired 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 keysimage.repository is cleaner than imageRepository
  • Provide sensible defaults — the chart should work with zero overrides
  • Use enabled: false for 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.txt is special — it's rendered and printed to the user after install/upgrade, but not sent to Kubernetes.
  • Files in tests/ are used by helm 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 dependencies
  • values.yaml — configurable defaults
  • templates/ — Go templates rendered into K8s manifests
  • _helpers.tpl — reusable template snippets
  • NOTES.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!