Helm Mission: Industrialize a Multi-Environment Deployment
14 min
Project 13 — Helm on Kubernetes · Level intermediate → advanced · Estimated duration: 4 to 6 h
You start from an application made of two Python services (a visual portail and an api backend), and you will deploy it three times side by side — in DEV (blue), STAGING (orange), PROD (green) — with a single Helm Chart and three values files. At the end, you will run a helm upgrade then a helm rollback, and you will repair three templates stuffed with real bugs seen in companies.
You work in a team where every new application version must go through three environments:
DEV — developers’ sandbox, disposable data, a single replica.
STAGING — pre-production, test data, two replicas to validate scalability.
PROD — production, real data, three replicas minimum, no downtime tolerated.
Today, the team copy-pastes the same YAML manifests for each environment, changing the different values by hand. Result: the files drift, a patch applied in dev does not show up in prod, and a deployment takes half a day.
Your mission: industrialize all of that with Helm. One Chart, three values files, one command per environment. You will prove it works by displaying three dashboards side by side in your browser — each with its own color, its own Pod count, and its own message.
Essential concepts before you start
This document is self-contained. You need no external reference to finish it.
1. Helm’s role in one sentence
Helm generates Kubernetes manifests from templates and variables. Where kubectl apply takes a static YAML, Helm takes a YAML template and a values file, produces the final YAML, then applies it as a versioned unit called a release.
Files whose name starts with _ produce no manifest: they define reusable “helpers” via {{ include "nom" . }}.
3. Template syntax (Go template)
Written
Rendered
{{ .Values.portail.replicas }}
The value defined in values.yaml
{{ .Release.Name }}
The name you passed to helm install (e.g. hedge-dev)
{{ .Chart.Name }}
The chart name (defined in Chart.yaml)
{{ .Chart.AppVersion }}
The application version (defined in Chart.yaml)
{{ include "hedge.labels" . }}
Call of a helper defined in _helpers.tpl
{{- ... -}}
The - strips spaces before/after the render
{{ .Values.env | quote }}
Adds quotes around the value
{{ .Values.replicas | default 1 }}
Uses 1 if the value is not set
4. The 5 Helm commands you will use
powershell
helm lint ./chart # check the syntaxhelm template <release> ./chart -f values-<env>.yaml # DRY render (no deployment)helm install <release> ./chart -f values-<env>.yaml -n <ns> # real deploymenthelm upgrade <release> ./chart -f values-<env>.yaml -n <ns> # incremental changehelm rollback <release> <revision> -n <ns> # go back
5. The keyword “release”
A release is one installation of a chart. The same chart can be installed several times, each with a different release name (hedge-dev, hedge-staging, hedge-prod) — that is the foundation of multi-environment.
{{ .Release.Name }} changes on every install, {{ .Chart.Name }} stays identical. Remember this contrast: it is central.
6. The golden rule of the immutable selector
The spec.selector.matchLabels field of a Deployment is fixed once and for all at creation. If your template puts in that field a value that can change (such as a version, an environment, a date), the first helm install will work, but the first helm upgrade will fail with:
spec.selector: Invalid value: ...: field is immutable
Rule to carve in stone: in matchLabels, put only things that will NEVER change for this instance — typically name, instance, component.
Target architecture: DEV / STAGING / PROD
You will deploy the same chart in three distinct namespaces, each with its parameters:
Parameter
DEV
STAGING
PROD
Namespace
hedge-dev
hedge-staging
hedge-prod
Release name
hedge-dev
hedge-staging
hedge-prod
Banner color
blue#2563eb
orange#ea580c
green#16a34a
Message
“Development environment…”
“Pre-production — test data only”
“Production — every action has a real impact”
portail replicas
1
2
3
api replicas
1
2
3
Exposed port (NodePort)
30130
30131
30132
Test URL
http://localhost:30130
http://localhost:30131
http://localhost:30132
At the end of the lab, you open three tabs side by side and you see three dashboards colored differently, each showing its environment, its version, its Pods, and the state of its backend.
File layout
You start from the following tree — the ANNEX provides the exact content of each file:
projet13-kubernetes-helm-tp/├── 00-ENONCE.md <- this document│├── apps/ <- THE CODE (ANNEX A) — DO NOT MODIFY│ ├── portail/│ │ ├── app.py│ │ ├── requirements.txt│ │ └── Dockerfile│ └── api/│ ├── app.py│ ├── requirements.txt│ └── Dockerfile│├── chart/ <- THE CHART TO COMPLETE│ ├── Chart.yaml <- skeleton (ANNEX B)│ ├── values.yaml <- default values (ANNEX B)│ ││ ├── environments/ <- YOUR TURN│ │ ├── values-dev.yaml <- TODO skeleton (ANNEX B)│ │ ├── values-staging.yaml <- TODO skeleton (ANNEX B)│ │ └── values-prod.yaml <- TODO skeleton (ANNEX B)│ ││ ├── templates/ <- YOUR TURN│ │ ├── _helpers.tpl <- TODO skeleton (ANNEX B)│ │ ├── portail-deployment.yaml <- TODO skeleton (ANNEX B)│ │ ├── portail-service.yaml <- TODO skeleton (ANNEX B)│ │ ├── api-deployment.yaml <- TODO skeleton (ANNEX B)│ │ └── api-service.yaml <- TODO skeleton (ANNEX B)│ ││ └── casses/ <- PROVIDED but DEFECTIVE (ANNEX C)│ ├── casse-1-configmap.yaml│ ├── casse-2-worker-deployment.yaml│ └── casse-3-cache-deployment.yaml│├── outils/│ └── valider.ps1 <- PROVIDED (ANNEX D)│└── RAPPORT.md <- TO WRITE by you
Crucial point: the files in chart/casses/ are not in chart/templates/. Helm therefore does not load them automatically. Mission 6 will ask you to copy them one by one into templates/ to observe the bug, then repair them before keeping them.
The rules of the game
Absolute ban on modifying the apps/ folder. The application code is already written — you are the DevOps, not the developer.
You modify only the files in the chart/ folder.
No environmental configuration hard-coded in a template: replicas, nodePort, color, message, environment — everything must come from a .Values.*.
The 3 values-<env>.yaml files must differ only by the values that distinguish DEV, STAGING, and PROD. A values-prod.yaml file that uselessly redefines image.repository or service.targetPort is an error — those things come from values.yaml.
You work on the Kubernetes built into Docker Desktop.
Preparation
Prerequisites — to check once only
Docker Desktop is started and Kubernetes is enabled (Settings → Kubernetes → Enable Kubernetes).
Docker Desktop has at least 4 GB of RAM allocated (Settings → Resources → Memory ≥ 4 GB). This lab runs 12 Pods at the same time (1+1 + 2+2 + 3+3).
portail-deployment.yaml — a Deployment that uses .Values.portail.replicas, .Values.portail.image.*, and injects the environment variables ENVIRONMENT, APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, BACKEND_URL, REPLICAS_INFO.
portail-service.yaml — a NodePort Service that points to the portal Pods.
api-deployment.yaml — a Deployment for the api (variables ENVIRONMENT, APP_VERSION).
api-service.yaml — a ClusterIP Service.
Key point: the portal’s BACKEND_URL variable must contain the name of the api Service built with {{ .Release.Name }} (for example http://hedge-dev-api), not a hard-coded name.
Validation:
powershell
helm template check .\chart -f .\chart\environments\values-dev.yaml# must display 2 Deployments + 2 Services, all prefixed by "check-"
Mission 3 — Write clean helpers (15 points)
Complete chart/templates/_helpers.tpl with three helpers:
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portail
Consult the history:
powershell
helm history hedge-dev -n hedge-dev
You see at least 2 revisions.
Cancel the upgrade by going back to revision 1:
powershell
helm rollback hedge-dev 1 -n hedge-dev
Check that we are back to a single portal Pod, and that the history shows a new revision of type Rollback:
powershell
kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portailhelm history hedge-dev -n hedge-dev
Question to answer in the report: what is the fundamental difference between helm upgrade --set portail.replicas=5 and kubectl scale deploy/hedge-dev-portail --replicas=5? Why does Helm prefer that you go through it?
The folder chart/casses/ contains three templates already written that compile but each introduce a real bug encountered in companies. For each one you must:
copy it into chart/templates/;
reproduce the symptom described at the top of the file;
diagnose the cause by reading the error message;
repair (by changing the template in chart/templates/, not the original in casses/);
prove that the outage has disappeared.
File
Component added
Nature of the bug
casse-1-configmap.yaml
A global ConfigMap
Name collision between releases
casse-2-worker-deployment.yaml
A workerDeployment
Immutable selector violated on the first helm upgrade
casse-3-cache-deployment.yaml
A cacheDeployment
Wrong value path (silent typo)
Investigation tip:
powershell
# DRY render of a single template (installs nothing)helm template hedge-dev .\chart -f .\chart\environments\values-dev.yaml ` --show-only templates/casse-3-cache-deployment.yaml --debug
This command prints exactly what Helm would send to Kubernetes. It is your first diagnostic tool — use it without restraint.
Mission 7 — Bonus: the refinement (5 points)
Choose one only:
a) Add a pre-install hook (Job) that displays Bienvenue dans <environnement> in the Helm logs. The release must wait for the Job to finish before continuing.
b) Make the replica count dynamic with a values.yaml value that has a nested structure (for example portail.autoscaling.enabled, portail.autoscaling.min, portail.autoscaling.max) and conditionally generate a HorizontalPodAutoscaler according to .enabled.
c) Add a NOTES.txt in templates/ that displays, after each helm install, the exact URL to open the dashboard (with the right nodePort according to the Values).
Copy these files, then replace each TODO with the right value.
Lines starting with # ? are questions to decide: you decide how to complete the code.
File: chart/Chart.yaml
yaml
# ? Fill in the mandatory fields of a Helm Chart.# ? apiVersion must be v2 (v1 has been deprecated since Helm 3).# ? type is "application" (as opposed to "library").apiVersion: TODOname: hedgedescription: TODOtype: TODOversion: 0.1.0appVersion: "1.0.0"
File: chart/values.yaml
yaml
# values.yaml — default values of the hedge chart.# Each environment provides a values-<env>.yaml file that OVERRIDES# these values on top. Keep this file NEUTRAL (no value specific# to one environment).environment: defaultbanniere: message: "Chart Helm — application multi-environnement" couleur: "#64748b"portail: image: repository: hedge-portail tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: NodePort port: 80 targetPort: 5000 nodePort: 30130api: image: repository: hedge-api tag: "1.0" pullPolicy: IfNotPresent replicas: 1 service: type: ClusterIP port: 80 targetPort: 8000
File: chart/templates/_helpers.tpl
yaml
{{/*Full name of a resource: "<release>-<composant>".Usage: {{ include "hedge.fullname" (dict "root" . "composant" "portail") }}*/}}{{- define "hedge.fullname" -}}{{- printf "TODO" .root.Release.Name .composant | trunc 63 | trimSuffix "-" -}}{{- end -}}{{/*Labels common to all resources.Usage: {{ include "hedge.labels" (dict "root" . "composant" "portail") | nindent 4 }}*/}}{{- define "hedge.labels" -}}# ? fill in the 7 labels requested in Mission 3app.kubernetes.io/name: TODOapp.kubernetes.io/instance: TODO# ... continue ...{{- end -}}{{/*Selector labels: STABLE subset of the labels.Put here ONLY labels that will NEVER change for an instance.*/}}{{- define "hedge.selectorLabels" -}}# ? the 3 STRICTLY immutable labels only{{- end -}}
File: chart/templates/portail-deployment.yaml
yaml
# ? Portal Deployment. Use:# - .Values.portail.replicas# - .Values.portail.image.{repository,tag,pullPolicy}# - .Values.portail.service.targetPort# - .Values.environment# - .Values.banniere.{couleur,message}# - .Chart.AppVersion (for APP_VERSION)# - The NAME of the api Service built with .Release.Name (for BACKEND_URL)apiVersion: apps/v1kind: Deploymentmetadata: name: TODO labels: TODOspec: replicas: TODO selector: matchLabels: TODO template: metadata: labels: TODO spec: containers: - name: portail image: TODO imagePullPolicy: TODO ports: - containerPort: TODO env: - name: ENVIRONMENT value: TODO # ? add APP_VERSION, THEME_COLOR, BANNIERE_MESSAGE, # BACKEND_URL, REPLICAS_INFO readinessProbe: httpGet: path: /health port: TODO initialDelaySeconds: 3 periodSeconds: 5
File: chart/templates/portail-service.yaml
yaml
# ? Service for the portal. Type NodePort. Use the condition# {{- if eq .Values.portail.service.type "NodePort" }} ... {{- end }}# to include "nodePort" ONLY if it really is a NodePort.apiVersion: v1kind: Servicemetadata: name: TODO labels: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO # ? nodePort only if type == NodePort
File: chart/templates/api-deployment.yaml
yaml
# ? Same structure as portail-deployment.yaml, but:# - component = "api"# - environment variables: ENVIRONMENT and APP_VERSION only# - container port = .Values.api.service.targetPort (8000)apiVersion: apps/v1kind: Deploymentmetadata: name: TODOspec: # ... (structure similar to the portal) ...
File: chart/templates/api-service.yaml
yaml
# ? ClusterIP Service for the api. A single port. No nodePort.apiVersion: v1kind: Servicemetadata: name: TODOspec: type: TODO selector: TODO ports: - port: TODO targetPort: TODO
File: chart/environments/values-dev.yaml
yaml
# ? DEV environment: 1 replica, blue banner #2563eb, NodePort 30130.environment: TODObanniere: message: TODO couleur: TODOportail: replicas: TODO service: nodePort: TODOapi: replicas: TODO
File: chart/environments/values-staging.yaml
yaml
# ? STAGING environment: 2 replicas, orange banner #ea580c, NodePort 30131.environment: TODO# ... complete on the model of values-dev.yaml ...
Each file below is in chart/casses/. Do not modify the originals — copy them into chart/templates/, reproduce the bug, then fix the copy.
File: chart/casses/casse-1-configmap.yaml
yaml
# OUTAGE 1# Symptom: deploy hedge-dev, then try to deploy hedge-staging# IN THE SAME NAMESPACE. The second install fails with:# ConfigMap "hedge-config" ... exists and cannot be imported ...apiVersion: v1kind: ConfigMapmetadata: name: hedge-config labels: {{- include "hedge.labels" (dict "root" . "composant" "config") | nindent 4 }}data: timezone: "America/Toronto" langue: "fr-CA"