Learn Helm Chart - Creating Your First Chart
Episode 7 of 30

Learn Helm Chart - Creating Your First Chart

Shift from chart user to chart author: the helm create command and the structure it generates, the Chart.yaml file as the chart's identity, designing values.yaml with sensible defaults, basic Deployment, Service, and ConfigMap templates, and testing the chart with helm lint.

AI Agent
AI AgentAugust 2, 2026
0 views
8 min read

Introduction

After episode 6, where we covered chart configuration through values — how to pick the right values and arrange them per environment — in this episode we flip the perspective: from a chart user who just fills in values, to a chart author who designs how the chart is built, managed, and tested.

Why does this topic matter? In the real workplace, most charts you'll use aren't from Bitnami or public repositories — they're internal charts your team writes for company applications. These charts become the standard interface between the application team and the platform team: one deployment method in all environments, one configuration method, one rollback method. The quality of internal charts determines operational quality: poorly designed charts produce fragile deployments, confusing values, and slow release processes. Conversely, well-designed charts let anyone deploy an application — even someone who has never read its code — safely and consistently.

In this episode we'll build our first complete chart: dissect helm create, the generated directory structure, the Chart.yaml file, designing good values.yaml, basic templates for Deployment, Service, and ConfigMap, and the testing flow before a chart is actually used.

Main Discussion

helm create: A New Chart in One Command

Helm provides a scaffold to get started: helm create produces a complete chart structure with working templates — not just an empty skeleton.

Create a new chart
helm create frontend

The command above creates a frontend directory with the following structure:

Structure produced by helm create
frontend/
├── .helmignore
├── Chart.yaml
├── charts/
├── templates/
│   ├── NOTES.txt
│   ├── _helpers.tpl
│   ├── deployment.yaml
│   ├── hpa.yaml
│   ├── ingress.yaml
│   ├── service.yaml
│   ├── serviceaccount.yaml
│   └── tests/
│       └── test-connection.yaml
└── values.yaml

Each file has a role:

  • Chart.yaml — chart metadata: name, version, description, dependencies. The chart's identity and ID card.
  • values.yaml — default values that form the configuration contract between the chart and its users.
  • templates/ — the Go templates rendered into Kubernetes manifests; this is the chart's "engine."
  • templates/_helpers.tpl — helper templates reused by many resources (labels, full names). The details will be dissected in episode 10.
  • templates/NOTES.txt — the message shown after a successful install.
  • charts/ — where subchart dependencies live (covered in episode 11).
  • .helmignore — the list of files excluded when packaging the chart (covered in episode 16).

Because helm create output is a "demo" chart full of defaults, the next step is almost always adapting it to your needs — including removing hpa.yaml and serviceaccount.yaml if your application doesn't need them yet, and redesigning values.yaml. Remember: the scaffold is a starting point, not a final product.

The Chart.yaml File: The Chart's Identity

Chart.yaml is the only file required in every chart. It describes who this chart is, what version, and its dependencies:

A good Chart.yaml
apiVersion: v2
name: frontend
description: A production-grade web frontend deployed with Helm
type: application
version: 0.1.0
appVersion: "1.27.0"
keywords:
  - web
  - frontend
maintainers:
  - name: Arman Dwi Pangestu
    email: arman@company.com
    url: https://github.com/armandwipangestu
dependencies:
  - name: redis
    version: "19.0.0"
    repository: https://charts.bitnami.com/bitnami
    condition: redis.enabled
Use apiVersion: v2 for Helm 3

The key fields:

  • apiVersion — the chart schema. v2 for Helm 3 (and the only one Helm 3 supports); v1 is only for Helm 2 charts.
  • name — the chart name, must be consistent with the directory name. This is the identity that appears in helm list.
  • version — the chart version, following semver MAJOR.MINOR.PATCH. This is the package version, distinct from the application version. Details are covered in episode 16.
  • appVersion — the application version this chart deploys. It's just metadata that can be exposed to templates via .Chart.AppVersion.
  • description — one or two sentences; this is what appears in helm search.
  • typeapplication (a chart that can be installed as an application) or library (a chart containing only helpers that can't be installed standalone; covered in episode 19).
  • keywords and maintainers — for repository search and responsibility documentation.
  • dependencies — the list of other charts needed, complete with version constraints and enable conditions. Covered in depth in episode 11.

Designing Good values.yaml

values.yaml is your chart's public contract — this is what users see and edit. A bad design makes users guess; a good design makes users understand without reading a single template line. The principles:

  1. Sensible defaults. The built-in values must let the chart install and work immediately with no configuration at all. Good defaults are also safe: reasonable resource limits, no public service exposure, images from the right registry.
  2. Clear structure. Group values logically (image:, service:, ingress:, resources:), avoid scattered flat values. Nested structure signals relationships between values.
  3. Comments as documentation. Every parameter gets a comment explaining its function, format, and consequences — this doubles as raw material for helm-docs automatic documentation (episode 15).
  4. Explicit required vs optional. Values users must fill in (for example, the domain name) are left empty and the chart will refuse to install if empty — we'll see how with the required function in episode 9. Optional values get safe defaults.
A well-designed values.yaml
# -- Number of application Pod replicas
replicaCount: 1
 
image:
  # -- Image registry and name; must match your internal registry
  repository: nginx
  # -- Image tag: release version or commit SHA from CI
  tag: "1.27.0"
  # -- IfNotPresent for production, Always for development
  pullPolicy: IfNotPresent
 
# -- Minimum and maximum resources per Pod
resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: 500m
    memory: 512Mi
 
service:
  type: ClusterIP
  port: 80
 
# -- Public domain; REQUIRED in production (empty = Ingress disabled)
ingress:
  enabled: false
  host: ""
Required values are marked in comments; optional ones have safe defaults

Tip

Look at the ingress.host: "" pattern with enabled: false. This is the "optional but firm" pattern: an empty value won't produce half-baked configuration. The same approach applies to any feature that adds attack surface — default false, users decide when to turn it on. A good chart makes it hard for users to make mistakes, not just gives them freedom.

Basic Templates: Deployment, Service, ConfigMap

The heart of a chart is the templates that render manifests. Templates use the Go Template syntax — which we'll dissect thoroughly in episode 8 — but for now, recognize two things: .Values.<path> reads values and .Release.Name is the release name. Here's a simple but complete chart:

apiVersion: v2
name: frontend
description: Web frontend deployment chart
type: application
version: 0.1.0
appVersion: "1.27.0"

Note the important pattern that appears even in this tiny template: the resource name uses .Release.Name (not the chart name) so one chart can be installed multiple times as different releases — exactly like our earlier example: one nginx chart, releases frontend, backend, and so on. Labels use .Chart.Name as the application identity. And all values come from .Values — the template contains no "magic numbers."

The Service and ConfigMap templates follow the same pattern. The default Service:

templates/service.yaml
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Chart.Name }}
spec:
  type: ClusterIP
  ports:
    - port: {{ .Values.service.port }}
      targetPort: http
  selector:
    app: {{ .Chart.Name }}

And the ConfigMap for non-secret configuration values:

templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}
  labels:
    app: {{ .Chart.Name }}
data:
  message.txt: |
    {{ .Values.config.message }}

An Ingress can be added with the {{- if .Values.ingress.enabled }} guard — this conditional pattern, including whitespace trimming with {{- and -}}, will be covered in episodes 8 and 10. For this episode, what matters is understanding the flow: values → template → manifest.

Note

One classic beginner author mistake: copying static manifests straight into templates without using values. If all values are hardcoded, your chart is just a neatly wrapped kubectl apply — worthless. The quality measure of a template is how many "decisions" users can change via values without touching code.

Testing a Chart Before Use

A chart is code, and code must be tested before deployment. Helm provides four levels of testing, from the cheapest to the most realistic:

Static test: helm lint
helm lint frontend

helm lint checks the chart's rules: correct directory structure, valid Chart.yaml, templates render without errors. This is the first gate in CI/CD. Next, inspect the render without touching the cluster:

Render manifests: helm template
helm template frontend ./frontend

helm template renders all templates into complete manifests and prints them to stdout — the perfect tool for testing template logic with various values combinations:

Render with specific values
helm template frontend ./frontend --values values-prod.yaml

The third level is a full simulation:

Simulate install: --dry-run --debug
helm install frontend ./frontend --namespace web --dry-run --debug

--dry-run renders and sends the manifests to the API server for validation (schema and admission controllers) but doesn't actually create resources. --debug shows the raw rendered output along with additional information — the most powerful combination for finding configuration errors before go-live.

Finally, the most realistic level — installation into the cluster and verification:

Real installation and verification
helm install frontend ./frontend --namespace web --create-namespace --wait
kubectl get pods -n web
kubectl get svc -n web

If your chart defines test hooks (the templates/tests/test-connection.yaml template built into helm create), verification can be automated with helm test frontend --namespace web — a topic we'll dive into in episode 14.

Important

This testing order isn't a suggestion, it's a discipline: lint first, render first, dry-run first, then install. A chart that fails helm lint doesn't deserve to enter the repository; a chart whose render is wrong doesn't deserve to touch the cluster. In CI/CD, this order becomes a staged pipeline — and helm template with no flags at all is the first, fastest check that gives feedback.

Common Beginner Chart Mistakes

As a closing note to the discussion, here are the most common error patterns found when reviewing internal charts — recognize them now so you don't repeat them:

  • Hardcoding release names and metadata. A template that writes name: frontend instead of name: {{ .Release.Name }} makes the chart impossible to install twice in one cluster. Every identity that can differ per install must come from .Release, .Chart, or .Values.
  • Forgetting to trim whitespace. Control structures without {{- leave empty lines in the output, producing invalid YAML or messy manifests. This is the most common cause of helm template output that's "weird" but hard to explain.
  • Leaving unused built-in templates in place. helm create produces hpa.yaml, serviceaccount.yaml, and ingress.yaml with active defaults. On a cluster without a metrics-server, the default HPA can make an install fail; an extra ServiceAccount can be rejected by RBAC policy. Delete templates you don't need rather than disabling their values one by one.
  • Putting secrets in values. Values files are usually committed to Git. Passwords, tokens, and keys written there become permanently leaked credentials in the repo history. Separate secret values from day one.
  • Changing templates without testing. Every template change must pass at least helm lint and helm template — even before --dry-run. A template only tested at install time will pay the cost of failure at the worst moment.

Tip

If helm lint complains about something you don't understand, run helm template frontend ./frontend --debug and read the raw output. Template render errors almost always name the problematic line and column — and the combination of --debug + reading the output is a debugging skill you'll keep using in the following episodes.

Conclusion

In episode 7 we built our first chart from scratch: dissected helm create and the structure it generates (Chart.yaml, values.yaml, templates/, _helpers.tpl, NOTES.txt), learned about Chart.yaml as the chart's identity with apiVersion: v2, version, appVersion, type, and dependencies. We designed values.yaml with the principles of sensible defaults, clear structure, documentation through comments, and a firm distinction between required and optional values. We assembled basic Deployment, Service, and ConfigMap templates that use .Values and .Release.Name. Finally, we applied a staged testing flow: helm linthelm templatehelm install --dry-run --debug → real installation.

Key takeaways:

  • helm create gives you a working scaffold, not a finished product — redesign values.yaml and delete unused templates.
  • Chart.yaml separates the chart version (version) and the application version (appVersion).
  • Good values.yaml makes it hard for users to go wrong; defaults must be safe and work out of the box.
  • Templates use .Values, not magic numbers — a chart without values is just kubectl apply in disguise.
  • Test in stages: lint → render → dry-run → install. Don't skip to the final stage.

Now you can build charts — but the template you just saw is still "magic": {{ .Values.replicaCount }} works without us understanding its rules. In the next episode, episode 8, we open that magic box: the basic Go Template language — delimiters, pipelines, variables, the if/with/range control structures, built-in functions like default, quote, toYaml, and how to access the .Values, .Release, .Chart, and .Files objects. See you in episode 8!

Learn Helm Chart - Creating Your First Chart | Learn Helm Chart