Creating a Custom Helm Chart for Django Microservices
Turn a directory of manifests into a versioned chart you can install into any environment with different values.
- Time
- 47 min
- Level
- Intermediate
- Objectives
- 4 objectives
- Cost
- Low cost
Where this fits in the platform
This lab adds
- The application as one versioned, installable chart
Before you start
Cost — Low cost
Depends on an existing cluster. Helm itself is free; the workloads it installs consume cluster capacity.
Nothing to pay in the browser. Open the terminal runs this against a simulated cloud — the same API calls and the same commands, with no account and no bill. The figure above applies only if you build it in your own.
The scenario#
There are three directories of manifests — dev, staging and prod — that started identical and are not any more. Nobody can say what differs except by diffing them, and the diff is 400 lines because the namespaces and image tags are on every file.
Hands-on environment
Run this lab in a real terminal, free and in your browser. The environment is temporary and yours alone — break it as much as you like.
Open the terminalOpens in Killercoda, in a new tab — keep this page open for the steps.
Run it on your own machine
Run this lab on your own machine. One command starts the environment, with everything the lab needs already installed:
Where the values file points at an ECR image, point it at a local image instead and load it with `kind load docker-image`.
You will need:
- docker
- kubectl
- kind
- helm
git clone https://github.com/EgyKode/EgyKode-lab.git
cd EgyKode-lab
./egykode start k8s
./egykode shellYou need Docker and Git installed. Everything else runs inside the environment. The first start downloads it and takes a few minutes; later starts are seconds.
Not sure what you already have? Run: npm run doctor — it checks and changes nothing.
Run it on AWS
This lab builds real cloud infrastructure, so it needs your own AWS account. Follow the cost and cleanup notes above — the resources are yours, and so is the bill.
Anything you tick here is your own record. EgyKode cannot see inside that terminal, so the success criteria stay self-assessed even when the environment checks your work for you.
Chart.yaml
Step 1 of 5
What you are building#
myapp/
Chart.yaml name, version, appVersion
values.yaml the defaults, and the documented interface
values-prod.yaml only what production differs by
templates/
_helpers.tpl names and labels, defined once
deployment.yaml
service.yaml
ingress.yaml
configmap.yaml
hpa.yaml
NOTES.txt printed after installA chart is a package plus a template engine plus a release. The last part
is what people underuse: Helm remembers what it installed, so upgrade, rollback
and diff are possible. kubectl apply -f has no memory of what it applied.
Build it#
What you are proving: You can define a chart's identity and the versions it carries
This step settles no success criterion on its own.
apiVersion: v2
name: myapp
description: The platform application
type: application
version: 0.3.0 # the CHART version — bump on template changes
appVersion: "1.4.2" # the APPLICATION version — the image tagTwo versions because they change for different reasons. Editing a template
without bumping version means two different charts share a number, and
helm history stops being able to tell you what ran.
What you are proving: You can define resource names once and reuse them across every template
This step settles no success criterion on its own.
{{/* templates/_helpers.tpl */}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "myapp.fullname" -}}
{{- printf "%s-%s" .Release.Name (include "myapp.name" .) | trunc 63 | trimSuffix "-" -}}
{{- end -}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ printf "%s-%s" .Chart.Name .Chart.Version }}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end -}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end -}}Selector labels are a separate helper on purpose. A Deployment's
spec.selector is immutable after creation, so it must not include
app.kubernetes.io/version or the chart version — the next appVersion bump
would try to change an immutable field and the upgrade fails with a message
that does not mention labels.
trunc 63 is not decoration either: Kubernetes label values are limited to 63
characters, and a long release name silently produces an invalid object.
What you are proving: You can write a template whose config change causes a rollout rather than mutating Pods that never restart
Marking this settles success criterion 3.
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "myapp.fullname" . }}
labels: {{- include "myapp.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels: {{- include "myapp.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
# Roll the Pods when the config changes. Without this the ConfigMap is
# updated and the running Pods keep the old values indefinitely.
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels: {{- include "myapp.selectorLabels" . | nindent 8 }}
spec:
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
envFrom:
- configMapRef:
name: {{ include "myapp.fullname" . }}-config
resources: {{- toYaml .Values.resources | nindent 12 }}
{{- with .Values.probes }}
readinessProbe: {{- toYaml .readiness | nindent 12 }}
livenessProbe: {{- toYaml .liveness | nindent 12 }}
{{- end }}The checksum/config annotation is the idiom worth memorising. Editing a
ConfigMap through Helm changes the object and does not restart anything —
so a config change appears to deploy and takes effect at some unpredictable
future restart. Hashing the rendered ConfigMap into the Pod template makes the
Pod spec change, which makes it a rollout.
{{- if not .Values.autoscaling.enabled }} around replicas matters once an
HPA exists: leaving replicas in the manifest makes Helm and the HPA fight,
and each helm upgrade resets the count.
What you are proving: You can install one chart into two environments through values alone, with no edited templates
Marking this settles success criterion 1.
replicaCount: 2
image:
repository: registry.example.com/api
tag: "" # defaults to .Chart.AppVersion
pullPolicy: IfNotPresent
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
probes:
readiness:
httpGet: { path: /healthz, port: 8000 }
periodSeconds: 5
liveness:
httpGet: { path: /healthz, port: 8000 }
periodSeconds: 10
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 6
targetCPUUtilizationPercentage: 70
ingress:
enabled: false
className: alb
hosts: []# values-prod.yaml — only the differences
replicaCount: 4
autoscaling: { enabled: true, maxReplicas: 12 }
ingress:
enabled: true
hosts: ["app.example.com"]
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { cpu: "2", memory: 1Gi }What you are proving: You can render and lint a chart before anything touches the cluster
Marking this settles success criteria 2 and 4.
helm lint ./myapp
helm template rel ./myapp -f myapp/values-prod.yaml | kubectl apply --dry-run=server -f -
helm install rel ./myapp -n platform --create-namespace -f myapp/values-prod.yaml --atomic --waithelm template renders locally with no cluster involved, so it is the fast
check; piping it to --dry-run=server adds validation against the real API,
including admission webhooks. Both belong in CI.
Verify it worked#
# Same chart, different environments, no edited templates
helm template rel ./myapp | grep -c "replicas: 2"
helm template rel ./myapp -f myapp/values-prod.yaml | grep -c "replicas: 4"
# What the release is ACTUALLY running, not what the file says
helm get values rel -n platform
helm get manifest rel -n platform | head -30
# A config change produces a rollout
kubectl get pods -n platform -o name > /tmp/before
helm upgrade rel ./myapp -n platform --set config.LOG_LEVEL=debug --atomic --wait
kubectl get pods -n platform -o name > /tmp/after
diff /tmp/before /tmp/after && echo "NO ROLLOUT — checksum annotation missing" || echo "rolled — correct"
helm history rel -n platformfield is immutable on upgrade
The Deployment selector changed, usually because a version label leaked into
selectorLabels. Selectors cannot be edited; the release must be uninstalled
and reinstalled.
A ConfigMap change did nothing
The checksum/config annotation is missing. The object updated; no Pod
restarted.
nindent produces broken YAML
Indentation is counted from column zero. nindent 4 adds a newline then
indents by 4 — indent does not add the newline. Rendering with
helm template shows it immediately.
Values from a previous release reappear
--reuse-values carries them forward. Use an explicit values file every time.
helm lint passes and the install fails
lint checks the chart, not the cluster. Add --dry-run=server, which runs
admission control.
Clean up#
Destructive — This removes real resources. Check which environment you are in first.
helm uninstall rel -n platform
kubectl delete namespace platformCost of this lab: Free on kind or minikube.
Maintained by others, on Killercoda. Useful for extra repetition on one tool — it does not complete this lab or settle any criterion above.
Success criteria
0 of 4
The concept behind it
Next up
Lab 41 of 59 on the project path
Previous: Kubernetes Security Hardening (NetworkPolicies) & HPA