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.

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.
helm create: A New Chart in One CommandHelm provides a scaffold to get started: helm create produces a complete chart structure with working templates — not just an empty skeleton.
helm create frontendThe command above creates a frontend directory with the following structure:
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.yamlEach 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.
Chart.yaml File: The Chart's IdentityChart.yaml is the only file required in every chart. It describes who this chart is, what version, and its dependencies:
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.enabledThe 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.type — application (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.values.yamlvalues.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:
image:, service:, ingress:, resources:), avoid scattered flat values. Nested structure signals relationships between values.helm-docs automatic documentation (episode 15).required function in episode 9. Optional values get safe defaults.# -- 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: ""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.
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:
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:
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.
A chart is code, and code must be tested before deployment. Helm provides four levels of testing, from the cheapest to the most realistic:
helm lint frontendhelm 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:
helm template frontend ./frontendhelm template renders all templates into complete manifests and prints them to stdout — the perfect tool for testing template logic with various values combinations:
helm template frontend ./frontend --values values-prod.yamlThe third level is a full simulation:
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:
helm install frontend ./frontend --namespace web --create-namespace --wait
kubectl get pods -n web
kubectl get svc -n webIf 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.
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:
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.{{- 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.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.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.
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 lint → helm template → helm 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).values.yaml makes it hard for users to go wrong; defaults must be safe and work out of the box..Values, not magic numbers — a chart without values is just kubectl apply in disguise.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!