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.


Table of contents


The context

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.

2. Anatomy of a Chart

mon-chart/
├── Chart.yaml            # metadata (name, version)
├── values.yaml           # DEFAULT values
└── templates/            # YAML templates
    ├── deployment.yaml
    ├── service.yaml
    └── _helpers.tpl      # shared template functions (name prefixed _)

Files whose name starts with _ produce no manifest: they define reusable “helpers” via {{ include "nom" . }}.

3. Template syntax (Go template)

WrittenRendered
{{ .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 syntax
helm template <release> ./chart -f values-<env>.yaml           # DRY render (no deployment)
helm install <release> ./chart -f values-<env>.yaml -n <ns>    # real deployment
helm upgrade <release> ./chart -f values-<env>.yaml -n <ns>    # incremental change
helm 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:

ParameterDEVSTAGINGPROD
Namespacehedge-devhedge-staginghedge-prod
Release namehedge-devhedge-staginghedge-prod
Banner colorblue #2563eborange #ea580cgreen #16a34a
Message“Development environment…”“Pre-production — test data only”“Production — every action has a real impact”
portail replicas123
api replicas123
Exposed port (NodePort)301303013130132
Test URLhttp://localhost:30130http://localhost:30131http://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

  1. Absolute ban on modifying the apps/ folder. The application code is already written — you are the DevOps, not the developer.
  2. You modify only the files in the chart/ folder.
  3. No environmental configuration hard-coded in a template: replicas, nodePort, color, message, environment — everything must come from a .Values.*.
  4. 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.
  5. You work on the Kubernetes built into Docker Desktop.

Preparation

Prerequisites — to check once only

  1. Docker Desktop is started and Kubernetes is enabled (Settings → Kubernetes → Enable Kubernetes).
  2. 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).
  3. Helm is installed:
    powershell
    helm version --short           # must show v3.x or v4.x
    Otherwise: winget install Helm.Helm (or choco install kubernetes-helm).
  4. You are on the right cluster:
    powershell
    kubectl config use-context docker-desktop
    kubectl get nodes              # docker-desktop   Ready

Building the images

The chart references two local images that you must build once only:

powershell
docker build -t hedge-portail:1.0 .\apps\portail
docker build -t hedge-api:1.0     .\apps\api

docker images | Select-String "^hedge"      # must show the 2 images

Reminder: Docker Desktop shares its daemon with Kubernetes; no “load” step is needed (unlike kind or minikube).


The missions

Mission 1 — Bring a minimal Chart to life (10 points)

Complete chart/Chart.yaml (name, apiVersion, type, version, appVersion). Then check:

powershell
helm lint .\chart
# must display: 1 chart(s) linted, 0 chart(s) failed

Expected: a Chart that passes lint without error.


Mission 2 — Template portail and api (20 points)

Complete the 4 files in chart/templates/:

  • 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:

  1. hedge.fullname — returns {{ .Release.Name }}-<composant> (e.g. hedge-dev-portail).
  2. hedge.labels — returns the standard labels:
    • app.kubernetes.io/name
    • app.kubernetes.io/instance
    • app.kubernetes.io/component
    • app.kubernetes.io/managed-by
    • app.kubernetes.io/version
    • helm.sh/chart
    • hedge/environment
  3. hedge.selectorLabels — returns only name, instance, component (the 3 labels guaranteed immutable for this instance).

Strong constraint: use these helpers in all your templates. No hard-coded resource name, no label copied by hand.

Tip: to pass several values to a helper, use a dict:

yaml
{{ include "hedge.labels" (dict "root" . "composant" "portail") | nindent 4 }}

The helper then receives .root.Release.Name, .root.Values..., and .composant.


Mission 4 — Three environments side by side (20 points)

Create the 3 files in chart/environments/ — each one redefines only the values that distinguish its environment.

Fileenvironmentreplicas (portail + api)nodePort (portail)ColorSuggested message
values-dev.yamldev130130#2563eb“Development environment — watch out, everything can change”
values-staging.yamlstaging230131#ea580c“Pre-production — test data only”
values-prod.yamlprod330132#16a34a“Production — every action has a real impact”

Deploying the 3 environments:

powershell
helm install hedge-dev     .\chart -f .\chart\environments\values-dev.yaml     -n hedge-dev     --create-namespace
helm install hedge-staging .\chart -f .\chart\environments\values-staging.yaml -n hedge-staging --create-namespace
helm install hedge-prod    .\chart -f .\chart\environments\values-prod.yaml    -n hedge-prod    --create-namespace

Wait until the Pods are ready (~30 s):

powershell
kubectl wait --for=condition=ready pod --all -n hedge-dev     --timeout=120s
kubectl wait --for=condition=ready pod --all -n hedge-staging --timeout=120s
kubectl wait --for=condition=ready pod --all -n hedge-prod    --timeout=120s

Open the 3 dashboards:

powershell
start http://localhost:30130       # DEV — blue banner, 1 replica
start http://localhost:30131       # STAGING — orange banner, 2 replicas
start http://localhost:30132       # PROD — green banner, 3 replicas

Expected: three pages of different colors, each showing its env, its version, its Pods, and its backend in green OK.


Mission 5 — Upgrade then rollback (10 points)

Simulate a production incident, then cancel it.

Scenario:

  1. In DEV, set portail.replicas to 5:
    powershell
    helm upgrade hedge-dev .\chart -f .\chart\environments\values-dev.yaml --set portail.replicas=5 -n hedge-dev
  2. Check that 5 portal Pods are running:
    powershell
    kubectl get pods -n hedge-dev -l app.kubernetes.io/component=portail
  3. Consult the history:
    powershell
    helm history hedge-dev -n hedge-dev
    You see at least 2 revisions.
  4. Cancel the upgrade by going back to revision 1:
    powershell
    helm rollback hedge-dev 1 -n hedge-dev
  5. 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=portail
    helm 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?


Mission 6 — Investigation: repair 3 defective templates (20 points)

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:

  1. copy it into chart/templates/;
  2. reproduce the symptom described at the top of the file;
  3. diagnose the cause by reading the error message;
  4. repair (by changing the template in chart/templates/, not the original in casses/);
  5. prove that the outage has disappeared.
FileComponent addedNature of the bug
casse-1-configmap.yamlA global ConfigMapName collision between releases
casse-2-worker-deployment.yamlA worker DeploymentImmutable selector violated on the first helm upgrade
casse-3-cache-deployment.yamlA cache DeploymentWrong 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).

Automatic validation

A script gives you your score at any time:

powershell
.\outils\valider.ps1

Example output on a partially done job:

[OK]    Mission 1 - Valid Chart............... 10/10
[OK]    Mission 2 - Basic templating.......... 20/20
[FAIL]  Mission 3 - Helpers and labels........  0/15   -> helper hedge.selectorLabels missing
[OK]    Mission 4 - Three environments........ 20/20
[FAIL]  Mission 5 - Upgrade + rollback........  0/10   -> no rollback detected in the history
[OK]    Mission 6 - Repairs (3 outages)....... 14/20   -> casse-3: typo .Values.portal still present

AUTOMATIC SCORE : 64 / 95

If PowerShell refuses to run the script (script execution is disabled), use:

powershell
powershell -ExecutionPolicy Bypass -File .\outils\valider.ps1

Deliverables

  1. The complete chart/ folder, in working order (clean helm lint).
  2. A RAPPORT.md containing:
    • the output of helm list -A showing your 3 releases;
    • one screenshot per environment (3 colored dashboards);
    • the full history of hedge-dev (with upgrade + rollback);
    • for each outage of mission 6: diagnostic command, cause, fix, proof;
    • your answers to the reflection questions.
  3. The final output of .\outils\valider.ps1.

Reflection questions

  1. Why is the spec.selector.matchLabels field immutable in Kubernetes? What problem does this constraint solve?
  2. You have 3 environments today. Tomorrow, the DevSecOps team asks for a 4th (“pre-prod”). Which files do you create and which ones do you not touch?
  3. What is the difference between helm upgrade --set replicas=5 and kubectl scale, from the point of view of traceability and rollback?
  4. The portal displays “backend OK” — why is that information more reliable than a simple kubectl get svc api?
  5. What happens if you delete a Pod with kubectl delete pod, while it was created by a Deployment via Helm? Is Helm aware of the “loss”?
  6. A colleague suggests putting app.kubernetes.io/version: {{ .Chart.AppVersion }} in the matchLabels of a Deployment. What do you answer?

Grading

ItemPoints
Mission 1 — Valid Chart and clean lint10
Mission 2 — Templating portail + api20
Mission 3 — Reusable helpers and labels15
Mission 4 — Three environments side by side20
Mission 5 — Traced upgrade + rollback10
Mission 6 — Diagnosis + repair of the 3 outages20
Report quality and justification of choices5
Bonus — Mission 7+5
Total100 (+5)

Penalties:

  • −10 per environmental value hard-coded in a template (replicas: 3 literal instead of .Values....).
  • −5 per useless redefinition in a values-<env>.yaml (a value that has no reason to differ between environments).
  • −10 per modification of a file in apps/.

Helm toolbox

powershell
# ANALYSIS (no deployment)
helm lint .\chart                                                        # syntax + best practices
helm template <release> .\chart -f <values.yaml>                         # full render
helm template <release> .\chart -f <values.yaml> --show-only templates/<fichier>   # targeted render
helm template <release> .\chart -f <values.yaml> --debug                 # with traces
helm show values .\chart                                                 # default values

# DEPLOYMENT
helm install <release> .\chart -f <values.yaml> -n <ns> --create-namespace
helm upgrade <release> .\chart -f <values.yaml> -n <ns>
helm upgrade <release> .\chart -f <values.yaml> --set portail.replicas=5 -n <ns>
helm rollback <release> <revision> -n <ns>
helm uninstall <release> -n <ns>

# OBSERVATION
helm list -A                                                             # all releases
helm status <release> -n <ns>
helm history <release> -n <ns>
helm get values <release> -n <ns>                                        # the active values
helm get manifest <release> -n <ns>                                      # the applied manifests

The 3 reflexes in case of a bug:

  1. Always start with helm template — it is a DRY render, with no risk, that shows exactly what will be sent to Kubernetes.
  2. Read the path in the error message — Helm always gives the file + the line + the faulty .Values.* path.
  3. helm get manifest shows you what is currently in the cluster (useful to compare with what your new template generates).


ANNEX A — The applications

Do not modify any of these files. Copy them as-is to the indicated paths.

A.1 — The portal (multi-environment dashboard)

All displayed values come from environment variables injected by Helm. The same image behaves differently according to the Deployment env:.

File: apps/portail/app.py

python
"""Portal — multi-environment dashboard.

This Pod displays the environment it runs in (DEV / STAGING / PROD),
the application version, the replica count, and the backend state.

All displayed values come from ENVIRONMENT VARIABLES injected
by Helm from values-<env>.yaml. The same code adapts to each
environment with no modification.
"""

import os
import socket
import time
import urllib.error
import urllib.request

from flask import Flask, jsonify, request

app = Flask(__name__)
DEMARRAGE = time.time()


def cfg():
    return {
        "env": os.environ.get("ENVIRONMENT", "inconnu"),
        "version": os.environ.get("APP_VERSION", "0.0.0"),
        "theme": os.environ.get("THEME_COLOR", "#64748b"),
        "message": os.environ.get("BANNIERE_MESSAGE", "Deploye avec Helm"),
        "backend_url": os.environ.get("BACKEND_URL", "http://api"),
        "replicas_info": os.environ.get("REPLICAS_INFO", "?"),
        "pod": socket.gethostname(),
        "uptime": int(time.time() - DEMARRAGE),
    }


def tester_backend(url):
    try:
        with urllib.request.urlopen(url + "/ping", timeout=1.5) as reponse:
            corps = reponse.read(200).decode("utf-8", "ignore")
        return "ok", corps.strip()
    except urllib.error.HTTPError as err:
        return "http", "HTTP %s" % err.code
    except Exception as err:
        return "ko", type(err).__name__


@app.route("/health")
def health():
    return "OK", 200


@app.route("/api-json")
def api_json():
    """Route useful for automatic validation."""
    c = cfg()
    etat, detail = tester_backend(c["backend_url"])
    return jsonify(pod=c["pod"], env=c["env"], version=c["version"],
                   backend=etat, backend_detail=detail, uptime=c["uptime"])


@app.route("/")
def accueil():
    c = cfg()
    etat, detail = tester_backend(c["backend_url"])
    # ... (full HTML template in the file — not repeated here to stay readable)

The complete file is provided in apps/portail/app.py.

File: apps/portail/requirements.txt

text
flask==3.0.3

File: apps/portail/Dockerfile

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]

A.2 — The api (simple backend)

File: apps/api/app.py

python
"""API — simple backend for the Helm example."""

import os
import socket
import time

from flask import Flask, jsonify

app = Flask(__name__)
DEMARRAGE = time.time()

ENV = os.environ.get("ENVIRONMENT", "inconnu")
VERSION = os.environ.get("APP_VERSION", "0.0.0")


@app.route("/")
@app.route("/ping")
def ping():
    return jsonify(service="api", env=ENV, version=VERSION,
                   pod=socket.gethostname(),
                   uptime=int(time.time() - DEMARRAGE))


@app.route("/health")
def health():
    return "OK", 200


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=8000)

File: apps/api/requirements.txt

text
flask==3.0.3

File: apps/api/Dockerfile

dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
CMD ["python", "app.py"]


ANNEX B — Chart skeleton

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: TODO
name: hedge
description: TODO
type: TODO
version: 0.1.0
appVersion: "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: default

banniere:
  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: 30130

api:
  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 3
app.kubernetes.io/name: TODO
app.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/v1
kind: Deployment
metadata:
  name: TODO
  labels:
    TODO
spec:
  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: v1
kind: Service
metadata:
  name: TODO
  labels:
    TODO
spec:
  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/v1
kind: Deployment
metadata:
  name: TODO
spec:
  # ... (structure similar to the portal) ...

File: chart/templates/api-service.yaml

yaml
# ? ClusterIP Service for the api. A single port. No nodePort.
apiVersion: v1
kind: Service
metadata:
  name: TODO
spec:
  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: TODO

banniere:
  message: TODO
  couleur: TODO

portail:
  replicas: TODO
  service:
    nodePort: TODO

api:
  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 ...

File: chart/environments/values-prod.yaml

yaml
# ? PROD environment: 3 replicas, green banner #16a34a, NodePort 30132.
environment: TODO
# ...


ANNEX C — The three outages to repair

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: v1
kind: ConfigMap
metadata:
  name: hedge-config
  labels:
    {{- include "hedge.labels" (dict "root" . "composant" "config") | nindent 4 }}
data:
  timezone: "America/Toronto"
  langue: "fr-CA"

File: chart/casses/casse-2-worker-deployment.yaml

yaml
# OUTAGE 2
# Symptom: "helm install" works. "helm upgrade" with a
# --set environment=recette fails with:
#   spec.selector: Invalid value: ...: field is immutable
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "hedge.fullname" (dict "root" . "composant" "worker") }}
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: {{ .Chart.Name }}
      app.kubernetes.io/instance: {{ .Release.Name }}
      app.kubernetes.io/component: worker
      hedge/environment: {{ .Values.environment | quote }}
  template:
    metadata:
      labels:
        {{- include "hedge.labels" (dict "root" . "composant" "worker") | nindent 8 }}
    spec:
      containers:
        - name: worker
          image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"

File: chart/casses/casse-3-cache-deployment.yaml

yaml
# OUTAGE 3
# Symptom: "helm install" fails with:
#   Error: template ...at <.Values.portal.replicas>:
#   nil pointer evaluating interface {}.replicas
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "hedge.fullname" (dict "root" . "composant" "cache") }}
spec:
  replicas: {{ .Values.portal.replicas }}
  selector:
    matchLabels:
      {{- include "hedge.selectorLabels" (dict "root" . "composant" "cache") | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "hedge.selectorLabels" (dict "root" . "composant" "cache") | nindent 8 }}
    spec:
      containers:
        - name: cache
          image: "{{ .Values.api.image.repository }}:{{ .Values.api.image.tag }}"


ANNEX D — The validation script

The file outils/valider.ps1 is provided as-is. It gives no solution — only a score and the first point to fix. Run it at any time:

powershell
.\outils\valider.ps1

What the script checks:

MissionAutomatic criteria
1helm lint passes, Chart.yaml has apiVersion: v2, type: application
2helm template really produces 2 Deployments and 2 Services, names prefixed by the release
3_helpers.tpl defines the 3 helpers, standard labels present
4The 3 values-<env>.yaml files exist with the right values (env, replicas, port, color), the 3 releases are deployed
5The hedge-dev release has ≥ 2 revisions and a rollback in the history
6No hard-coded hedge-config, no variable label in matchLabels, no .Values.portal (with typo)

The script itself runs no helm install command: it is up to you to deploy before validating.


Course created by Dr. Haythem REHOUMA — Development and Deployment of Data Solutions