Helm From Scratch
Helm is a Kubernetes package manager and templating tool. For client work, start with a small chart, expose safe knobs in values.yaml, render locally, dry-run against the cluster, then install with helm upgrade --install --wait --atomic.
When To Use Helm
Use Helm when the same Kubernetes workload must be installed repeatedly with different names, environments, images, resources, ingress hosts, storage classes, or feature flags. Avoid Helm for one-off emergency patches where a direct manifest is clearer and the client does not want Helm to own the resource afterward.
| Need | Helm fit | Example |
|---|---|---|
| Install third-party platform tools | Excellent | cert-manager, ingress-nginx, external-secrets, prometheus |
| Deploy internal apps across environments | Excellent | same app chart with dev, staging, prod values |
| Generate many similar resources | Good | one Service and Deployment per app from a values map |
| Manage drift manually with kubectl | Poor | manual edits will be overwritten on next Helm upgrade |
Install Helm
Helm runs on your laptop, jumpbox, or CI runner. It talks to the cluster using the active kubeconfig context, just like kubectl.
# macOS
brew install helm
# Linux
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
kubectl config current-context
helm list -A
Create A Chart
helm create gives you a working chart. In client work, trim it down so the chart is easy to reason about and only exposes values your team actually supports.
helm create web-api
cd web-api
# Common cleanup for a simple app chart.
rm -rf templates/tests
rm -f templates/hpa.yaml templates/serviceaccount.yaml
tree -a .
Minimal Working Chart
This is the smallest useful flow: one Deployment, one Service, one values file, and one helper file. Build this first, then add ingress, config, secrets, PVCs, or autoscaling only when the client needs them.
replicaCount: 2
image:
repository: ghcr.io/example/web-api
tag: "1.0.0"
pullPolicy: IfNotPresent
service:
type: ClusterIP
port: 80
targetPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
The helper file defines reusable names and labels for the rest of the chart. Add it early so every Deployment, Service, Ingress, and ConfigMap uses the same naming convention.
{{- define "web-api.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "web-api.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
{{- else -}}
{{- printf "%s-%s" .Release.Name (include "web-api.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- end -}}
{{- define "web-api.labels" -}}
app.kubernetes.io/name: {{ include "web-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
{{- end -}}
{{- define "web-api.selectorLabels" -}}
app.kubernetes.io/name: {{ include "web-api.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}
Deployment And Service
These two templates create the workload and its stable internal network identity. Start with this pair for a stateless web/API service before adding Ingress, autoscaling, config, secrets, or storage.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "web-api.fullname" . }}
labels:
{{- include "web-api.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "web-api.selectorLabels" . | nindent 6 }}
template:
metadata:
labels:
{{- include "web-api.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ include "web-api.name" . }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.targetPort }}
resources:
{{- toYaml .Values.resources | nindent 12 }}
The Service exposes the Deployment inside the cluster and selects Pods using the same stable labels as the Deployment selector. Keep selector labels immutable after the first install.
apiVersion: v1
kind: Service
metadata:
name: {{ include "web-api.fullname" . }}
labels:
{{- include "web-api.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
selector:
{{- include "web-api.selectorLabels" . | nindent 4 }}
ports:
- name: http
port: {{ .Values.service.port }}
targetPort: http
protocol: TCP
Render And Validate
Do not learn a chart by installing it first. Render it, inspect the YAML, then let the API server validate it with a server-side dry run.
helm lint ./web-api --strict
helm template web-api ./web-api -n app-dev -f web-api/values.yaml > /tmp/web-api.yaml
kubectl apply --dry-run=server -f /tmp/web-api.yaml
# Debug template errors with source context.
helm template web-api ./web-api -n app-dev -f web-api/values.yaml --debug
Install, Upgrade, Rollback
Use upgrade --install so the same command works for first install and later changes. --atomic rolls back Kubernetes resources if the upgrade fails, but it cannot undo external side effects such as database migrations.
helm upgrade --install web-api ./web-api \
-n app-dev --create-namespace \
-f web-api/values.yaml \
--wait --atomic --timeout 10m
helm status web-api -n app-dev
helm history web-api -n app-dev
kubectl get deploy,svc,pod -n app-dev -l app.kubernetes.io/instance=web-api
# Roll back to a known-good revision during an incident.
helm rollback web-api 3 -n app-dev --wait --timeout 10m
Client Request Flow
| Client asks for | Add this to chart | Check after deploy |
|---|---|---|
| Expose app inside cluster | ClusterIP Service | kubectl get endpointslice |
| Expose app publicly | Ingress with TLS | Ingress address, cert, controller logs |
| Read app config | ConfigMap mounted as env or file | Pod env/file content and rollout checksum |
| Use password/token | Existing Secret or ExternalSecret reference | Secret exists; pod has env/mount |
| Persist data | PVC or StatefulSet volumeClaimTemplates | PVC Bound; volume mounted |
| Deploy several similar apps | Range over .Values.apps | One Deployment/Service per app |