Lab 10 — Package the shortener as a Helm chart and ship two environments
Lesson 10 · Cloud Infrastructure & DevOps · ~60 min
Prereqs
Before starting this lab, confirm the following are in place:
- L09 lab complete. You understand the raw manifests that the chart replaces.
- Helm v3 installed.
brew install helmon macOS, or follow helm.sh/docs/intro/install/. Verify withhelm version. kindandkubectlinstalled (same as L09).- Optional:
chart-testing.brew install chart-testing. CI does the heavy lifting, but locallyct lintis useful for catching errors before pushing.
Step 1 — Walk through the chart structure
Open sample-app/helm/ and tour each file. The chart was already authored as part of the lesson prep, so your job here is to understand what each piece does before running anything.
Chart.yaml — declares the chart name (shortener), version: 0.1.0 (the chart version, bumped on structural changes), and appVersion: "0.4.0" (the application version — informational, not used in image references directly).
values.yaml — the canonical defaults. Compare it with sample-app/k8s/api-deployment.yaml from L09: what was hard-coded (replicas: 2, the image tag, the probe paths) is now a value. The structure groups settings by component (api., worker., postgres.) rather than by manifest type.
templates/_helpers.tpl — defines three named templates: shortener.labels (the standard label block used on every object), shortener.api.fullname (resolves to {{ "{{" }} .Release.Name {{ "}}" }}-api), and shortener.worker.fullname. Every other template includes the labels block via {{- include "shortener.labels" . | nindent 4 }}.
templates/api-deployment.yaml — compare with k8s/api-deployment.yaml from L09. The three key changes:
replicas: {{ "{{" }} .Values.api.replicas {{ "}}" }}replaces the hard-coded2.- The probe block is wrapped in
{{- if .Values.api.probes.enabled }}so it can be toggled off. - The
resources:block uses{{ "{{-" }} toYaml .Values.api.resources | nindent 12 {{ "}}" }}to render the whole map from values.
templates/postgres-statefulset.yaml — the entire file is wrapped in {{- if .Values.postgres.enabled }}. When enabled: false (stage/prod using a managed database), Helm skips this template entirely and produces no Postgres objects. The emptyDir vs PVC choice is also conditional on .Values.postgres.storage.
templates/secret.yaml — guarded by {{- if .Values.secrets.inChart }}. Dev renders the Secret; stage/prod skip it and expect External Secrets Operator to populate it.
Step 2 — helm lint and helm template
Run the three commands from the repo root:
cd cloud-devops/sample-app
helm lint helm/
Expected: 1 chart(s) linted, 0 chart(s) failed. A note about the missing icon is expected and harmless.
helm template helm/ -f helm/values-dev.yaml | head -80
Inspect the first 80 lines. You should see the ConfigMap (rendered first alphabetically), then the Secret, then the API Deployment with replicas: 1.
helm template helm/ -f helm/values-prod.yaml | grep -E 'replicas:|host:'
Expected: replicas: 3 (API) and replicas: 2 (worker). The host: line does not appear yet — the Ingress template is added in Lesson 11.
Also confirm that prod drops the Postgres StatefulSet:
helm template helm/ -f helm/values-prod.yaml | grep -c 'kind: StatefulSet'
Expected: 0. And that the Secret is absent too:
helm template helm/ -f helm/values-prod.yaml | grep -c 'kind: Secret'
Expected: 0 (because prod.secrets.inChart: false).
Step 3 — Install dev on kind
kind create cluster --name shortener
helm upgrade --install shortener helm/ \
-f helm/values-dev.yaml \
--namespace shortener --create-namespace --atomic --wait --timeout 5m
helm status shortener -n shortener
helm history shortener -n shortener
Expected: helm status reports STATUS: deployed. helm history shows revision 1 with status deployed.
Why --atomic --wait?
Without --wait, helm upgrade --install returns as soon as the API server accepts the manifests — before pods are running. Without --atomic, a failed rollout leaves the cluster in a partial state and Helm reports success. These two flags are the difference between a deploy command you can trust in CI and one that gives you false positives.
Confirm the pods are running:
kubectl -n shortener get pods
Expected: one API pod, one worker pod, one Postgres pod, all Running or Completed.
Verify the API responds:
kubectl -n shortener port-forward svc/shortener-api 8080:80 &
curl -s http://localhost:8080/healthz
kill %1
Step 4 — Roll a new version
Edit helm/values-dev.yaml to bump the API tag:
api: {replicas: 1, tag: "0.4.1"}
Then upgrade:
helm upgrade --install shortener helm/ \
-f helm/values-dev.yaml \
--namespace shortener --atomic --wait --timeout 5m
Watch the rolling update in a second terminal:
kubectl -n shortener get pods -w
After the upgrade completes, check history:
helm history shortener -n shortener
Expected: two revisions. Revision 1 status is superseded; revision 2 is deployed.
Now roll back to revision 1:
helm rollback shortener 1 -n shortener
helm history shortener -n shortener
Expected: three revisions. Revision 3 status is deployed (the rollback is itself a revision); revision 2 is superseded.
Step 5 — Inspect rendered manifests inside the release
helm get manifest shortener -n shortener
This prints the exact YAML the cluster has — the rendered templates after all value merges. Compare with helm template: they should be identical.
helm get values shortener -n shortener
Prints only the user-supplied values (the -f values-dev.yaml overrides), not the full merged tree. Add --all to see the full merged values:
helm get values shortener -n shortener --all
These two commands replace kubectl get -o yaml for chart-installed releases. When something looks wrong in the cluster, start here to confirm the values you intended actually reached the rendered manifests.
Step 6 — Try the per-env override mechanism
Render stage without installing:
helm template helm/ -f helm/values-stage.yaml > /tmp/stage.yaml
grep 'kind:' /tmp/stage.yaml
Expected output shows: ConfigMap, Deployment (API), Service, Deployment (worker) — but no StatefulSet (postgres disabled) and no Secret (inChart: false). The Ingress template does not exist yet (L11 adds it), but the values already have ingress.enabled: true and host: set — ready for the L11 template to pick them up.
Confirm with:
grep -E 'StatefulSet|Secret' /tmp/stage.yaml | wc -l
Expected: 0.
Step 7 — Trigger the chart-testing workflow
Push a no-op change to sample-app/helm/values.yaml (add a comment, bump the chart description in Chart.yaml, etc.) to a PR targeting main. The Helm chart-testing workflow should trigger on the PR path filter sample-app/helm/**.
Look at the workflow log in GitHub Actions:
ct lintstep: validatesChart.yaml, runshelm lintagainstvalues.yaml, reportsLinting OK.ct installstep:helm/kind-actionspins up akindcluster, thenct installinstalls the chart, waits for pods to beReady, and optionally runs any test hooks (none yet — that’s Homework Task 1). Look for theInstalling chart 'shortener'line followed byInstallation of chart 'shortener' succeeded.
If ct reports “no chart changes detected”
ct uses git history to detect which charts changed. If the branch was force-pushed or the fetch depth in the workflow is too shallow, ct may skip the install step silently. Add a substantive change to Chart.yaml’s description field to force detection, and confirm the workflow has fetch-depth: 0.
Step 8 — Tear down
helm uninstall shortener -n shortener
kind delete cluster --name shortener
helm uninstall removes all objects Helm created in the namespace. The namespace itself persists; kubectl delete ns shortener if you want a clean slate.
What you should leave with
After completing this lab you should be able to:
- Explain the difference between a chart, a release, and a revision.
- Use
helm lint,helm template, andhelm install --dry-runto validate a chart before touching the cluster. - Run an atomic upgrade and understand what
--atomic --waitdoes differently from a plainhelm upgrade. - Roll back a release and read
helm history. - Explain how the per-env values files work and which objects each env renders.
Troubleshooting
helm installfails withtemplate: ... at <.Values.api.tag>: nil pointer— a per-env file omitted a key that the template uses without adefaultguard. Fix: add adefaultto the template, or add the missing key to the env file. The shortener chart already guards most values; you may see this if you add a custom per-env file that omits required keys.chart-testingerrors “no chart changes detected” — CT uses git history; on a force-pushed branch it sometimes can’t diff. Add a tiny change toChart.yaml’s description or bumpversion.- Stage render includes an Ingress reference but no ingress controller installed — expected; L11 installs
ingress-nginx. The values are already there; the template is not yet. The stage render is clean. helm upgrade --atomicrolls back unexpectedly — checkkubectl describeon the failed pod. Atomic mode is strict: any pod that doesn’t reachReadywithin--timeouttriggers rollback. Increase the timeout for slow images or fix the underlying readiness issue.