Lesson 10 Helm Chart for the Two Services
Theme:

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:

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:

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:

  1. ct lint step: validates Chart.yaml, runs helm lint against values.yaml, reports Linting OK.
  2. ct install step: helm/kind-action spins up a kind cluster, then ct install installs the chart, waits for pods to be Ready, and optionally runs any test hooks (none yet — that’s Homework Task 1). Look for the Installing chart 'shortener' line followed by Installation 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:

Troubleshooting