TP Kubernetes débutant avec minikube : Pod, Deployment, Service

Durée estimée : 2 h à 2 h 30 Niveau : débutant (aucune connaissance Kubernetes requise, notions de base Linux et Docker conseillées)

Objectifs

À la fin de ce TP, vous saurez :

  1. démarrer un cluster Kubernetes local avec minikube ;
  2. créer, inspecter et supprimer un Pod ;
  3. déployer une application avec un Deployment (réplicas, auto-réparation, mise à l'échelle, mise à jour, retour arrière) ;
  4. exposer cette application avec un Service (ClusterIP puis NodePort) ;
  5. comprendre le rôle des labels et des selectors, qui relient tous ces objets entre eux.

Sommaire


Partie 0 : Installation et démarrage

0.1 Prérequis

  • Une machine Linux, macOS ou Windows avec au moins 2 CPU, 4 Go de RAM libres et 20 Go de disque.
  • Docker installé et fonctionnel (minikube l'utilisera comme « driver »).

Vérifiez Docker :

docker version
docker run --rm hello-world
Sous Linux, si la commande docker demande sudo, ajoutez votre utilisateur au groupe docker puis reconnectez-vous :

0.2 Installer kubectl

kubectl est l'outil en ligne de commande qui parle à l'API Kubernetes.

Linux (amd64) :

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
kubectl version --client

macOS (Homebrew) :

brew install kubectl

Windows (winget) :

winget install -e --id Kubernetes.kubectl

0.3 Installer minikube

Linux (amd64) :

curl -LO https://github.com/kubernetes/minikube/releases/latest/download/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
rm minikube-linux-amd64
minikube version

macOS :

brew install minikube

Windows :

winget install -e --id Kubernetes.minikube

0.4 Démarrer le cluster

minikube start --driver=docker --cpus=2 --memory=4096

Le premier démarrage télécharge une image de quelques centaines de Mo, comptez 2 à 5 minutes.

Vérifiez l'état :

minikube status
kubectl get nodes

Résultat attendu :

NAME       STATUS   ROLES           AGE   VERSION
minikube   Ready    control-plane   1m    v1.3x.x
Ce qui se passe : minikube crée un conteneur Docker qui joue le rôle d'une machine virtuelle. Dans ce conteneur tournent tous les composants d'un cluster Kubernetes (API server, scheduler, etcd, kubelet...). Votre cluster contient donc un seul nœud, qui est à la fois le plan de contrôle et le nœud qui exécute vos applications. En production, un cluster comporte plusieurs nœuds, mais les commandes restent identiques.

0.5 Préparer un dossier de travail

mkdir -p ~/tp-k8s && cd ~/tp-k8s

Tous les fichiers YAML du TP seront créés dans ce dossier.


Partie 1 : Premiers pas avec kubectl

1.1 La logique de kubectl

Toutes les commandes suivent le même schéma :

kubectl <verbe> <type de ressource> [nom] [options]

Exemples : kubectl get pods, kubectl describe pod mon-pod, kubectl delete deployment web.

1.2 Explorer le cluster

kubectl cluster-info
kubectl get namespaces
kubectl get pods -A
Namespace : un espace de noms qui sert à ranger les ressources. Les composants internes de Kubernetes vivent dans kube-system. Vos ressources iront par défaut dans le namespace default.

L'option -A (ou --all-namespaces) affiche les ressources de tous les namespaces.

1.3 Obtenir de l'aide intégrée

kubectl api-resources          # liste tous les types de ressources et leurs abréviations
kubectl explain pod            # documentation d'un type
kubectl explain pod.spec.containers

kubectl explain est très utile pour savoir quels champs écrire dans un YAML.

1.4 (Optionnel) Autocomplétion

# bash
echo 'source <(kubectl completion bash)' >> ~/.bashrc
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
source ~/.bashrc

Partie 2 : Le Pod

2.1 Notion

Le Pod est la plus petite unité que Kubernetes sait exécuter. Un Pod contient un ou plusieurs conteneurs qui partagent :

  • la même adresse IP ;
  • les mêmes volumes éventuels ;
  • le même cycle de vie (ils démarrent et s'arrêtent ensemble).

Dans 95 % des cas, un Pod contient un seul conteneur. Retenez qu'on ne gère presque jamais les Pods à la main en production. On les crée ici pour comprendre la brique de base.

2.2 Créer un Pod en mode impératif

kubectl run nginx-test --image=nginx:1.27
kubectl get pods

Observez la colonne STATUS : ContainerCreating puis Running. Relancez kubectl get pods jusqu'à voir Running, ou suivez en direct :

kubectl get pods -w      # Ctrl+C pour quitter

2.3 Inspecter le Pod

kubectl get pods -o wide              # affiche l'IP du Pod et le nœud
kubectl describe pod nginx-test       # détail complet + événements
kubectl logs nginx-test               # sortie standard du conteneur

Dans la sortie de describe, lisez la section Events en bas : elle raconte l'histoire du Pod (planification sur un nœud, téléchargement de l'image, démarrage du conteneur). C'est le premier endroit à regarder quand quelque chose ne fonctionne pas.

2.4 Entrer dans le conteneur

kubectl exec -it nginx-test -- /bin/bash

Dans le conteneur :

hostname
cat /usr/share/nginx/html/index.html
exit

2.5 Accéder au Pod depuis votre poste

L'IP d'un Pod n'est joignable que depuis l'intérieur du cluster. Pour tester depuis votre machine, on ouvre un tunnel temporaire :

kubectl port-forward pod/nginx-test 8080:80

Dans un second terminal :

curl http://localhost:8080

Revenez au premier terminal et faites Ctrl+C pour fermer le tunnel.

port-forward est un outil de débogage. Ce n'est pas la manière d'exposer une application : c'est le rôle du Service (partie 4).

2.6 Le mode déclaratif : écrire un manifeste YAML

En pratique, on décrit les ressources dans des fichiers YAML versionnés dans Git. Supprimez d'abord le Pod précédent :

kubectl delete pod nginx-test

Créez le fichier pod.yaml :

apiVersion: v1            # version de l'API qui gère ce type d'objet
kind: Pod                 # type d'objet
metadata:
  name: whoami            # nom unique dans le namespace
  labels:                 # étiquettes clé/valeur, très importantes (voir plus bas)
    app: whoami
    env: tp
spec:                     # l'état souhaité
  containers:
    - name: whoami
      image: traefik/whoami:v1.10.1
      ports:
        - containerPort: 80
Image utilisée : traefik/whoami est un petit serveur web qui renvoie des informations sur la requête et, surtout, le nom du Pod qui a répondu. Ce sera très pratique pour observer la répartition de charge dans la partie Service.

Appliquez le fichier :

kubectl apply -f pod.yaml
kubectl get pods --show-labels

Testez :

kubectl port-forward pod/whoami 8080:80
# second terminal :
curl http://localhost:8080

La ligne Hostname: affiche whoami, le nom du Pod.

2.7 Les quatre champs de tout manifeste

Champ Rôle
apiVersion Version de l'API Kubernetes (v1, apps/v1...)
kind Type de ressource (Pod, Deployment, Service...)
metadata Identité : nom, namespace, labels
spec État souhaité : ce que vous voulez que Kubernetes fasse exister

Kubernetes compare en permanence l'état souhaité (spec) à l'état réel et agit pour les faire correspondre. Ce principe s'appelle la boucle de réconciliation et c'est le cœur de Kubernetes.

2.8 Expérience : un Pod seul ne se répare pas

Supprimez le Pod :

kubectl delete pod whoami
kubectl get pods

Le Pod a disparu et rien ne le recrée. Un Pod créé seul n'est surveillé par aucun contrôleur : s'il est supprimé ou si son nœud tombe, l'application est perdue. C'est la raison d'être du Deployment.

✅ Point de contrôle 2

Répondez sans regarder le corrigé :

  1. Quelle commande affiche l'IP d'un Pod ?
  2. Où trouver la raison d'un Pod bloqué en Pending ou ImagePullBackOff ?
  3. Que se passe-t-il quand on supprime un Pod créé seul ?

Partie 3 : Le Deployment

3.1 Notion

Un Deployment décrit une application à faire tourner :

  • quelle image utiliser ;
  • combien de copies (réplicas) garder en vie en permanence ;
  • comment effectuer les mises à jour.

Le Deployment ne crée pas directement les Pods. Il crée un ReplicaSet, qui crée et surveille les Pods :

Deployment  ──gère──►  ReplicaSet  ──gère──►  Pod, Pod, Pod
(stratégie de mise à jour)  (nombre de copies)     (conteneurs)

3.2 Créer le Deployment

Créez deployment.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3                     # nombre de Pods souhaités
  selector:                       # comment le Deployment reconnaît « ses » Pods
    matchLabels:
      app: web
  template:                       # modèle de Pod : même contenu qu'un Pod seul
    metadata:
      labels:
        app: web                  # DOIT correspondre au selector ci-dessus
    spec:
      containers:
        - name: whoami
          image: traefik/whoami:v1.10.1
          ports:
            - containerPort: 80
          resources:              # ressources réservées et plafonnées
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 200m
              memory: 64Mi
Explications :template est le modèle à partir duquel chaque Pod sera fabriqué. Son contenu est identique au metadata + spec du pod.yaml précédent.selector.matchLabels doit correspondre aux labels du template. Sinon Kubernetes refuse le Deployment.resources.requests indique ce que le Pod réserve sur le nœud (utilisé par le scheduler pour choisir où le placer). limits fixe un plafond : au-delà de la mémoire limite, le conteneur est tué (OOMKilled). 50m signifie 50 millièmes de CPU.

Appliquez :

kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get replicasets
kubectl get pods -o wide

Observez les noms des Pods, par exemple web-5d8f7c9b6d-x2k4p :

  • web : nom du Deployment ;
  • 5d8f7c9b6d : identifiant du ReplicaSet (lié à la version du template) ;
  • x2k4p : suffixe aléatoire propre au Pod.

Vous pouvez tout afficher d'un coup :

kubectl get deploy,rs,pods -l app=web

3.3 Expérience : l'auto-réparation

Ouvrez un second terminal et surveillez les Pods :

kubectl get pods -l app=web -w

Dans le premier terminal, supprimez un Pod (remplacez par un nom réel) :

kubectl delete pod web-xxxxxxxxxx-xxxxx

Dans le terminal de surveillance, un nouveau Pod apparaît immédiatement. Le ReplicaSet a constaté 2 Pods au lieu de 3 et en a recréé un. C'est la boucle de réconciliation en action.

3.4 Mise à l'échelle (scaling)

Méthode impérative (rapide, pour tester) :

kubectl scale deployment web --replicas=5
kubectl get pods -l app=web

Méthode déclarative (recommandée) : modifiez replicas: 3 en replicas: 4 dans deployment.yaml, puis :

kubectl apply -f deployment.yaml
kubectl get pods -l app=web
Le fichier YAML fait foi. Une modification impérative (kubectl scale) sera écrasée au prochain kubectl apply du fichier. Gardez l'habitude de modifier le fichier.

3.5 Mise à jour progressive (rolling update)

Passez à une nouvelle version de l'image. Dans deployment.yaml, remplacez :

          image: traefik/whoami:v1.10.1

par :

          image: traefik/whoami:v1.10.3

Appliquez et observez :

kubectl apply -f deployment.yaml
kubectl rollout status deployment web
kubectl get replicasets -l app=web

Vous voyez maintenant deux ReplicaSets : l'ancien, réduit à 0 Pod, et le nouveau avec 4 Pods.

Ce qui se passe : Kubernetes a créé un nouveau ReplicaSet et a remplacé les Pods progressivement (quelques nouveaux démarrent, quelques anciens s'arrêtent, et ainsi de suite). L'application reste disponible pendant toute la mise à jour. L'ancien ReplicaSet est conservé pour permettre un retour arrière.

Consultez l'historique :

kubectl rollout history deployment web
Pour renseigner la colonne CHANGE-CAUSE, ajoutez une annotation après chaque changement :
kubectl annotate deployment web kubernetes.io/change-cause="passage en v1.10.3" --overwrite

3.6 Expérience : une mise à jour ratée et le retour arrière

Simulons une erreur en déployant une version qui n'existe pas :

kubectl set image deployment/web whoami=traefik/whoami:v9.9.9
kubectl get pods -l app=web

Certains Pods restent en ErrImagePull puis ImagePullBackOff. Diagnostiquez :

kubectl describe pod <nom-d-un-pod-en-erreur>

Dans Events, un message indique que l'image est introuvable.

Constatez que les anciens Pods tournent toujours : Kubernetes n'a pas supprimé les Pods fonctionnels tant que les nouveaux n'étaient pas prêts. L'application reste en service.

Revenez à la version précédente :

kubectl rollout undo deployment web
kubectl rollout status deployment web
kubectl get pods -l app=web
Remettez deployment.yaml en cohérence : vérifiez qu'il contient bien v1.10.3. Sinon, le prochain apply produirait un résultat inattendu.

✅ Point de contrôle 3

  1. Quel objet crée réellement les Pods : le Deployment ou le ReplicaSet ?
  2. Pourquoi le selector doit-il correspondre aux labels du template ?
  3. Pendant la mise à jour ratée, pourquoi l'application restait-elle disponible ?

Partie 4 : Le Service

4.1 Le problème à résoudre

Listez les IP de vos Pods :

kubectl get pods -l app=web -o wide

Supprimez un Pod et relancez la commande : le Pod recréé a une nouvelle IP. Les IP de Pods sont éphémères. De plus, avec 4 Pods, à quelle IP un client doit-il s'adresser ?

4.2 Notion

Un Service fournit :

  • une IP virtuelle stable et un nom DNS stable ;
  • une répartition de charge entre tous les Pods qui portent les bons labels.

Le Service ne connaît pas les Pods par leur nom. Il les sélectionne par leurs labels, exactement comme le ReplicaSet :

           Service "web" (selector: app=web)
           IP stable : 10.96.x.x  /  DNS : web
                         │
         ┌───────────────┼───────────────┐
         ▼               ▼               ▼
   Pod app=web     Pod app=web     Pod app=web

Principaux types de Service :

Type Accessible depuis Usage
ClusterIP l'intérieur du cluster seulement communication entre applications (défaut)
NodePort l'extérieur, via IP_du_nœud:30000-32767 tests, environnements simples
LoadBalancer l'extérieur, via un répartiteur du fournisseur cloud production dans le cloud

4.3 Service de type ClusterIP

Créez service-clusterip.yaml :

apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  type: ClusterIP
  selector:
    app: web              # cible tous les Pods portant ce label
  ports:
    - port: 80            # port exposé par le Service
      targetPort: 80      # port du conteneur dans les Pods

Appliquez :

kubectl apply -f service-clusterip.yaml
kubectl get service web
kubectl get endpointslices -l kubernetes.io/service-name=web

La seconde commande liste les IP des Pods actuellement derrière le Service. Elle se met à jour automatiquement quand des Pods apparaissent ou disparaissent.

Testez depuis l'intérieur du cluster avec un Pod temporaire :

kubectl run client --rm -it --image=busybox:1.36 --restart=Never -- sh

Dans le Pod client :

wget -qO- http://web | grep Hostname
wget -qO- http://web | grep Hostname
wget -qO- http://web | grep Hostname
wget -qO- http://web.default.svc.cluster.local | grep Hostname
exit
Observations :Le nom web suffit : le DNS interne du cluster résout le nom du Service. Le nom complet est <service>.<namespace>.svc.cluster.local.La valeur Hostname change d'une requête à l'autre : le Service répartit les requêtes entre les Pods.--rm supprime le Pod client à la sortie.

4.4 Service de type NodePort

Pour atteindre l'application depuis votre poste, créez service-nodeport.yaml :

apiVersion: v1
kind: Service
metadata:
  name: web-nodeport
spec:
  type: NodePort
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080     # optionnel, doit être entre 30000 et 32767

Appliquez :

kubectl apply -f service-nodeport.yaml
kubectl get services

Récupérez l'URL d'accès :

minikube service web-nodeport --url
Avec le driver Docker sous macOS et Windows, cette commande ouvre un tunnel et doit rester active dans son terminal. Utilisez l'URL affichée depuis un second terminal. Sous Linux, l'URL est directement http://$(minikube ip):30080.

Testez plusieurs fois :

URL=$(minikube service web-nodeport --url)   # sous Linux ; sinon copiez l'URL affichée
for i in $(seq 1 8); do curl -s $URL | grep Hostname; done

La répartition entre les Pods est visible.

4.5 Expérience : le Service suit les Pods

Dans un terminal, lancez une boucle de requêtes :

while true; do curl -s $URL | grep Hostname; sleep 0.5; done

Dans un autre terminal, jouez avec le Deployment :

kubectl scale deployment web --replicas=2
kubectl scale deployment web --replicas=6
kubectl delete pod -l app=web --wait=false

Le Service continue de répondre et les noms de Pods évoluent au fil des changements. Aucune configuration du Service n'a été modifiée : c'est le selector qui fait le lien. Arrêtez la boucle avec Ctrl+C et remettez le Deployment à l'état du fichier :

kubectl apply -f deployment.yaml

4.6 Expérience : un selector erroné

Modifiez temporairement le selector du Service :

kubectl patch service web-nodeport -p '{"spec":{"selector":{"app":"wbe"}}}'
curl -s --max-time 3 $URL || echo "Pas de réponse"
kubectl get endpointslices -l kubernetes.io/service-name=web-nodeport

Plus aucun Pod ne correspond, la liste des endpoints est vide et le Service ne répond plus. C'est l'une des pannes les plus fréquentes chez les débutants. Réparez :

kubectl apply -f service-nodeport.yaml

✅ Point de contrôle 4

  1. Pourquoi ne faut-il pas utiliser l'IP d'un Pod directement ?
  2. Comment un Service sait-il vers quels Pods envoyer le trafic ?
  3. Quelle différence entre port, targetPort et nodePort ?

Partie 5 : Exercice de synthèse

Sans copier les fichiers précédents (utilisez kubectl explain si besoin), déployez une seconde application :

Consignes :

  1. Un fichier unique api.yaml contenant à la fois un Deployment et un Service (séparez les deux documents YAML par une ligne ---).
  2. Deployment nommé api, image nginx:1.27, 2 réplicas, label app: api, tier: backend.
  3. Service nommé api de type NodePort sur le port 31000, qui cible uniquement les Pods app: api.
  4. Vérifiez que curl sur l'URL NodePort renvoie la page d'accueil nginx.
  5. Mettez l'image à jour en nginx:1.28 en modifiant le fichier, puis vérifiez le déroulement avec kubectl rollout status.
  6. Depuis un Pod busybox temporaire, vérifiez que le Service api est joignable par son nom DNS.

Bonus :

  • Listez uniquement les Pods ayant le label tier=backend.

Affichez la version de l'image utilisée par chaque Pod avec :

kubectl get pods -l app=api -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[0].image}{"\n"}{end}'

Partie 6 : Nettoyage

Supprimez les ressources créées :

kubectl delete -f api.yaml
kubectl delete -f service-nodeport.yaml -f service-clusterip.yaml -f deployment.yaml
kubectl get all

Il ne doit rester que le Service kubernetes (Service interne, ne pas le supprimer).

Arrêter ou supprimer le cluster :

minikube stop       # arrête le cluster, les données sont conservées
minikube delete     # supprime complètement le cluster

Annexe A : Aide-mémoire kubectl

Action Commande
Lister des ressources kubectl get pods / deploy / svc / rs
Lister avec détails kubectl get pods -o wide
Filtrer par label kubectl get pods -l app=web
Afficher les labels kubectl get pods --show-labels
Suivre en direct kubectl get pods -w
Voir le détail et les événements kubectl describe pod <nom>
Logs d'un conteneur kubectl logs <pod> (-f pour suivre)
Shell dans un conteneur kubectl exec -it <pod> -- sh
Appliquer un fichier kubectl apply -f fichier.yaml
Supprimer via un fichier kubectl delete -f fichier.yaml
Voir le YAML d'un objet existant kubectl get deploy web -o yaml
Générer un YAML sans rien créer kubectl create deployment demo --image=nginx --dry-run=client -o yaml
Mettre à l'échelle kubectl scale deploy web --replicas=3
Suivre une mise à jour kubectl rollout status deploy web
Historique kubectl rollout history deploy web
Retour arrière kubectl rollout undo deploy web
Tunnel de test kubectl port-forward svc/web 8080:80
Documentation d'un champ kubectl explain deployment.spec.strategy
Événements du namespace kubectl get events --sort-by=.lastTimestamp

Abréviations courantes : po (pods), deploy (deployments), rs (replicasets), svc (services), ns (namespaces), no (nodes).


Annexe B : Dépannage

Symptôme Cause probable Vérification
Pod en Pending Ressources insuffisantes sur le nœud kubectl describe pod, section Events
ErrImagePull / ImagePullBackOff Nom ou tag d'image incorrect, pas d'accès réseau kubectl describe pod
CrashLoopBackOff Le conteneur démarre puis plante en boucle kubectl logs <pod> --previous
OOMKilled Dépassement de limits.memory kubectl describe pod, champ Last State
Service ne répond pas Selector qui ne correspond à aucun Pod kubectl get endpointslices -l kubernetes.io/service-name=<svc>
minikube start échoue Docker arrêté ou mémoire insuffisante docker ps, puis minikube delete && minikube start
kubectl ne trouve pas le cluster Contexte incorrect kubectl config current-context (doit afficher minikube)

Tableau de bord graphique (facultatif, pratique pour visualiser) :

minikube dashboard

Annexe C : Corrigés

Point de contrôle 2

  1. kubectl get pods -o wide (ou kubectl describe pod <nom>).
  2. Dans la section Events de kubectl describe pod <nom>.
  3. Il disparaît définitivement : aucun contrôleur ne le surveille ni ne le recrée.

Point de contrôle 3

  1. Le ReplicaSet. Le Deployment gère les ReplicaSets (un par version du template) et orchestre le passage de l'un à l'autre.
  2. Le selector permet au Deployment (et au ReplicaSet) d'identifier les Pods qu'il possède. S'il ne correspondait pas aux labels du template, les Pods créés ne seraient jamais comptés et le contrôleur en créerait à l'infini. L'API refuse donc cette configuration.
  3. La stratégie RollingUpdate ne supprime les anciens Pods qu'à mesure que les nouveaux deviennent prêts. Les nouveaux ne démarrant jamais, les anciens ont été conservés.

Point de contrôle 4

  1. Elle change à chaque recréation du Pod, et elle ne désigne qu'un seul Pod parmi plusieurs réplicas.
  2. Par son selector : il envoie le trafic à tous les Pods prêts dont les labels correspondent.
  3. port : port du Service (IP stable interne). targetPort : port du conteneur dans le Pod. nodePort : port ouvert sur chaque nœud pour l'accès depuis l'extérieur.

Exercice de synthèse : api.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  labels:
    app: api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
        tier: backend
    spec:
      containers:
        - name: nginx
          image: nginx:1.27      # passer à nginx:1.28 à l'étape 5
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              cpu: 200m
              memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  type: NodePort
  selector:
    app: api
  ports:
    - port: 80
      targetPort: 80
      nodePort: 31000

Commandes :

kubectl apply -f api.yaml
kubectl get deploy,svc,pods -l app=api
curl -s $(minikube service api --url) | grep -i "welcome to nginx"

# Étape 5 : modifier l'image dans api.yaml en nginx:1.28, puis
kubectl apply -f api.yaml
kubectl rollout status deployment api

# Étape 6
kubectl run client --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://api

# Bonus
kubectl get pods -l tier=backend

TP Kubernetes : déployer WordPress avec des volumes persistants et des Services NodePort

Durée estimée : 3 h 30 à 4 h Niveau : débutant, suite du TP « Pod, Deployment, Service » Prérequis : minikube et kubectl installés, notions de Pod, Deployment, Service, labels et selectors

Objectifs

À la fin de ce TP, vous saurez :

  1. comprendre pourquoi un conteneur perd ses données et comment les volumes règlent ce problème ;
  2. demander du stockage persistant avec un PersistentVolumeClaim (PVC) ;
  3. stocker des mots de passe dans un Secret ;
  4. déployer une application en deux tiers : WordPress + base de données MariaDB ;
  5. faire communiquer deux applications par un Service ClusterIP ;
  6. exposer WordPress (et phpMyAdmin en exercice) avec des Services NodePort ;
  7. vérifier que les données survivent à la suppression des Pods ;
  8. migrer la base de données vers un StatefulSet et comprendre ce qui le distingue d'un Deployment.

Sommaire


Partie 0 : Préparation

0.1 Démarrer minikube avec les ports NodePort publiés

Dans ce TP, WordPress sera exposé sur le NodePort 30090 et phpMyAdmin sur 30091.

WordPress enregistre dans sa base l'adresse utilisée lors de l'installation et redirige ensuite toujours vers elle. Il faut donc que l'URL d'accès soit stable. Pour y arriver simplement, on publie les NodePorts directement sur votre machine au démarrage du cluster.

Si un cluster minikube existe déjà, supprimez-le (les ports ne peuvent être publiés qu'à la création) :

minikube delete

Puis démarrez :

minikube start --driver=docker --cpus=2 --memory=4096 --ports=30090:30090,30091:30091

Vérifiez :

kubectl get nodes
minikube addons list | grep -E "storage-provisioner|default-storageclass"

Les deux addons storage-provisioner et default-storageclass doivent être enabled. Ce sont eux qui fabriqueront automatiquement le stockage demandé par nos PVC.

Adresse d'accès retenue pour tout le TP : http://localhost:30090 Avec l'option --ports, cette adresse fonctionne sous Linux, macOS et Windows.

0.2 Dossier de travail et namespace

mkdir -p ~/tp-wordpress && cd ~/tp-wordpress
kubectl create namespace wordpress
kubectl config set-context --current --namespace=wordpress
kubectl config view --minify | grep namespace
Pourquoi un namespace ? Toutes les ressources du TP seront rangées dans le namespace wordpress. La deuxième commande en fait le namespace par défaut : plus besoin d'ajouter -n wordpress à chaque commande. Le nettoyage final se fera en supprimant ce seul namespace.

Partie 1 : Architecture cible

            Votre navigateur
                   │  http://localhost:30090
                   ▼
   ┌─────────────────────────────────┐
   │ Service "wordpress" (NodePort)  │  port 30090 sur le nœud
   └───────────────┬─────────────────┘
                   ▼
   ┌─────────────────────────────────┐        ┌──────────────────────┐
   │ Deployment "wordpress"          │───────►│ PVC "wordpress-data"  │
   │ Pod : wordpress (Apache + PHP)  │        │ /var/www/html         │
   └───────────────┬─────────────────┘        └──────────────────────┘
                   │  mariadb:3306
                   ▼
   ┌─────────────────────────────────┐
   │ Service "mariadb" (ClusterIP)   │  interne au cluster uniquement
   └───────────────┬─────────────────┘
                   ▼
   ┌─────────────────────────────────┐        ┌──────────────────────┐
   │ Deployment "mariadb"            │───────►│ PVC "mariadb-data"    │
   │ Pod : mariadb                   │        │ /var/lib/mysql        │
   └─────────────────────────────────┘        └──────────────────────┘

   Secret "wordpress-db" : mots de passe lus par les deux Deployments

Points clés :

  • MariaDB n'est pas exposée à l'extérieur : un Service ClusterIP suffit, seul WordPress doit la joindre.
  • WordPress est exposé par un Service NodePort pour être accessible depuis le navigateur.
  • Chaque application a son propre volume persistant : la base de données d'un côté, les fichiers WordPress (thèmes, extensions, médias) de l'autre.

Fichiers que vous allez créer :

Fichier Contenu
secret.yaml Secret des mots de passe
mariadb.yaml PVC + Deployment + Service de MariaDB
wordpress.yaml PVC + Deployment + Service de WordPress
phpmyadmin.yaml exercice de la partie 7
mariadb-statefulset.yaml MariaDB en StatefulSet (partie 8)

Partie 2 : Comprendre les volumes

2.1 Le problème : un conteneur est éphémère

Le système de fichiers d'un conteneur est jetable. Quand le conteneur redémarre ou que le Pod est recréé, tout ce qui a été écrit dedans est perdu. Vérifions-le.

Créez test-ephemere.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: test-ephemere
spec:
  containers:
    - name: box
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
kubectl apply -f test-ephemere.yaml
kubectl wait --for=condition=Ready pod/test-ephemere
kubectl exec test-ephemere -- sh -c 'echo "donnée importante" > /data.txt && cat /data.txt'

Recréez le Pod :

kubectl delete pod test-ephemere
kubectl apply -f test-ephemere.yaml
kubectl wait --for=condition=Ready pod/test-ephemere
kubectl exec test-ephemere -- cat /data.txt

Résultat : No such file or directory. Le fichier a disparu avec l'ancien Pod. Pour une base de données, ce serait la perte de tous les articles.

kubectl delete pod test-ephemere

2.2 Les objets de stockage de Kubernetes

Objet Rôle Qui le crée
Volume (dans le Pod) Monte un espace de stockage dans un conteneur, à un chemin donné Vous, dans le spec du Pod
PersistentVolumeClaim (PVC) Une demande de stockage : « je veux 1 Go en lecture/écriture » Vous
PersistentVolume (PV) Le stockage réel (disque, partage réseau, dossier du nœud...) L'administrateur, ou automatiquement par une StorageClass
StorageClass Un « type » de stockage capable de créer des PV à la demande L'administrateur du cluster (minikube en fournit une : standard)

La logique :

Pod ──monte──► volume ──référence──► PVC ──lié à──► PV ──► stockage réel
                                    (demande)        (fourni par la StorageClass)

L'intérêt de ce découpage : le développeur exprime un besoin (taille, mode d'accès) dans le PVC sans connaître la technologie de stockage. Le même manifeste fonctionne sur minikube, sur un cloud public ou sur un cluster OpenStack, seule la StorageClass change.

Regardez la StorageClass de minikube :

kubectl get storageclass

Sur minikube, elle crée les PV sous forme de dossiers à l'intérieur du nœud (/tmp/hostpath-provisioner/...).

2.3 Premier PVC : vérifier la persistance

Créez test-pvc.yaml :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-pvc
spec:
  accessModes:
    - ReadWriteOnce          # monté en lecture/écriture par un seul nœud à la fois
  resources:
    requests:
      storage: 100Mi         # taille demandée
---
apiVersion: v1
kind: Pod
metadata:
  name: test-persistant
spec:
  containers:
    - name: box
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: donnees      # nom du volume déclaré plus bas
          mountPath: /data   # chemin où il apparaît dans le conteneur
  volumes:
    - name: donnees
      persistentVolumeClaim:
        claimName: test-pvc  # le PVC à utiliser
Le lien entre les deux blocs : volumes (au niveau du Pod) déclare d'où vient le stockage. volumeMounts (au niveau du conteneur) indique où le monter. Les deux sont reliés par le même name.
kubectl apply -f test-pvc.yaml
kubectl get pvc
kubectl get pv

Le PVC est en statut Bound : un PV a été créé automatiquement et lui a été attribué.

Refaites l'expérience :

kubectl wait --for=condition=Ready pod/test-persistant
kubectl exec test-persistant -- sh -c 'echo "donnée importante" > /data/data.txt'
kubectl delete pod test-persistant
kubectl apply -f test-pvc.yaml
kubectl wait --for=condition=Ready pod/test-persistant
kubectl exec test-persistant -- cat /data/data.txt

Cette fois, le fichier est toujours là : il est stocké dans le PV, pas dans le conteneur.

Curiosité : voyez le fichier directement sur le nœud minikube :

minikube ssh -- sudo find /tmp/hostpath-provisioner -name data.txt

Nettoyez :

kubectl delete -f test-pvc.yaml
kubectl get pv

Le PV a été supprimé avec le PVC. C'est la politique de récupération (reclaimPolicy) Delete de la StorageClass standard. Avec Retain, le PV et ses données seraient conservés. Retenez-le : supprimer un PVC peut supprimer les données.

2.4 Les modes d'accès

Mode Abréviation Signification
ReadWriteOnce RWO lecture/écriture, un seul nœud à la fois
ReadOnlyMany ROX lecture seule, plusieurs nœuds
ReadWriteMany RWX lecture/écriture, plusieurs nœuds (partage réseau type NFS, CephFS)
ReadWriteOncePod RWOP lecture/écriture, un seul Pod

Nous utiliserons ReadWriteOnce, le mode le plus courant et le seul proposé par la StorageClass de minikube.

✅ Point de contrôle 2

  1. Pourquoi un fichier écrit dans un conteneur disparaît-il quand le Pod est recréé ?
  2. Quelle est la différence entre un PVC et un PV ?
  3. Que se passe-t-il pour les données quand on supprime un PVC dont la StorageClass a reclaimPolicy: Delete ?

Partie 3 : Le Secret des mots de passe

3.1 Notion

Un Secret stocke des informations sensibles (mots de passe, clés d'API, certificats) séparément des manifestes applicatifs. Les Pods les lisent sous forme de variables d'environnement ou de fichiers.

Attention : par défaut, un Secret est seulement encodé en base64, pas chiffré. Toute personne ayant le droit de lire les Secrets du namespace peut lire les mots de passe. En production, on restreint cet accès (RBAC), on active le chiffrement au repos d'etcd et on ne versionne pas les Secrets en clair dans Git.

3.2 Créer le Secret

Créez secret.yaml :

apiVersion: v1
kind: Secret
metadata:
  name: wordpress-db
type: Opaque
stringData:                        # valeurs en clair, Kubernetes les encode lui-même
  root-password: RootTP-2026!
  database: wordpress
  user: wpuser
  password: WpUserTP-2026!
kubectl apply -f secret.yaml
kubectl get secret wordpress-db
kubectl describe secret wordpress-db

describe affiche la taille des valeurs mais pas leur contenu. Pour constater que ce n'est qu'un encodage :

kubectl get secret wordpress-db -o jsonpath='{.data.password}' | base64 -d; echo

Partie 4 : La base de données MariaDB

4.1 Le manifeste

Créez mariadb.yaml. Il contient trois objets séparés par --- :

# 1) Demande de stockage pour les fichiers de la base
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mariadb-data
  labels:
    app: mariadb
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
---
# 2) Le Deployment de MariaDB
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  replicas: 1                      # une base = une seule instance ici
  strategy:
    type: Recreate                 # arrêter l'ancien Pod AVANT de démarrer le nouveau
  selector:
    matchLabels:
      app: mariadb
  template:
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
        - name: mariadb
          image: mariadb:11.4
          ports:
            - containerPort: 3306
          env:                                  # variables lues par l'image MariaDB au 1er démarrage
            - name: MARIADB_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wordpress-db            # nom du Secret
                  key: root-password            # clé dans le Secret
            - name: MARIADB_DATABASE
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: database
            - name: MARIADB_USER
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: user
            - name: MARIADB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: password
          volumeMounts:
            - name: data
              mountPath: /var/lib/mysql         # dossier où MariaDB stocke ses fichiers
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              memory: 512Mi
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: mariadb-data
---
# 3) Service interne : nom DNS stable "mariadb" sur le port 3306
apiVersion: v1
kind: Service
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  type: ClusterIP
  selector:
    app: mariadb
  ports:
    - port: 3306
      targetPort: 3306
Explications :valueFrom.secretKeyRef injecte une valeur du Secret dans une variable d'environnement. Le mot de passe n'apparaît jamais dans mariadb.yaml.strategy: Recreate : la stratégie par défaut (RollingUpdate) démarre le nouveau Pod avant d'arrêter l'ancien. Deux instances MariaDB écriraient alors dans les mêmes fichiers, ce qui peut corrompre la base. Avec Recreate, il y a une courte interruption mais jamais deux instances simultanées.replicas: 1 : une base de données ne se met pas à l'échelle en augmentant simplement les réplicas. Chaque instance aurait besoin de ses propres données et d'une réplication entre elles (voir la partie 8 sur les StatefulSets).Le Service est en ClusterIP : la base reste invisible depuis l'extérieur du cluster, ce qui est la bonne pratique de sécurité.

4.2 Déployer et vérifier

kubectl apply -f mariadb.yaml
kubectl get pvc,pods,svc -l app=mariadb
kubectl rollout status deployment mariadb
kubectl logs deploy/mariadb | tail -n 5

Dans les logs, attendez la ligne indiquant ready for connections.

Testez la connexion avec l'utilisateur applicatif :

kubectl exec -it deploy/mariadb -- mariadb -u wpuser -p'WpUserTP-2026!' -e "SHOW DATABASES;"

La base wordpress doit apparaître.

Testez la résolution DNS du Service depuis un autre Pod :

kubectl run dnstest --rm -it --image=busybox:1.36 --restart=Never -- nslookup mariadb
Piège classique : les variables MARIADB_* ne sont lues qu'au tout premier démarrage, quand le dossier /var/lib/mysql est vide. Si vous modifiez le mot de passe dans le Secret après coup, la base garde l'ancien. Il faut alors soit changer le mot de passe en SQL, soit supprimer le PVC pour repartir de zéro (et perdre les données).

✅ Point de contrôle 4

  1. Pourquoi MariaDB utilise-t-elle un Service ClusterIP plutôt que NodePort ?
  2. Pourquoi la stratégie Recreate plutôt que RollingUpdate ?

Partie 5 : WordPress

5.1 Le manifeste

Créez wordpress.yaml :

# 1) Stockage des fichiers WordPress (cœur, thèmes, extensions, médias)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: wordpress-data
  labels:
    app: wordpress
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 2Gi
---
# 2) Le Deployment WordPress
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  replicas: 1
  strategy:
    type: Recreate                 # volume RWO : un seul Pod à la fois
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
        - name: wordpress
          image: wordpress:6.8-apache
          ports:
            - containerPort: 80
          env:
            - name: WORDPRESS_DB_HOST
              value: mariadb:3306               # nom du Service MariaDB + port
            - name: WORDPRESS_DB_NAME
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: database
            - name: WORDPRESS_DB_USER
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: user
            - name: WORDPRESS_DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: password
          volumeMounts:
            - name: html
              mountPath: /var/www/html
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              memory: 512Mi
      volumes:
        - name: html
          persistentVolumeClaim:
            claimName: wordpress-data
---
# 3) Service NodePort : accès depuis l'extérieur du cluster
apiVersion: v1
kind: Service
metadata:
  name: wordpress
  labels:
    app: wordpress
spec:
  type: NodePort
  selector:
    app: wordpress
  ports:
    - port: 80              # port du Service dans le cluster
      targetPort: 80        # port d'Apache dans le conteneur
      nodePort: 30090       # port ouvert sur le nœud (plage 30000-32767)
Explications :WORDPRESS_DB_HOST: mariadb:3306 : WordPress joint la base par le nom du Service. Il ne connaît ni le nom ni l'IP du Pod MariaDB, qui peuvent changer à tout moment./var/www/html est monté sur le volume. Au premier démarrage, l'image copie les fichiers WordPress dans ce dossier vide et génère wp-config.php. Ensuite, les extensions installées et les médias téléversés y sont conservés.Le Service NodePort ouvre le port 30090 sur le nœud minikube. Grâce à l'option --ports du démarrage, ce port est aussi publié sur votre machine.

5.2 Rappel : les trois ports d'un Service NodePort

Navigateur ──► localhost:30090 ──► nœud:30090 (nodePort)
                                       │
                                       ▼
                         Service wordpress:80 (port)
                                       │
                                       ▼
                         Pod wordpress:80 (targetPort)

5.3 Déployer et vérifier

kubectl apply -f wordpress.yaml
kubectl rollout status deployment wordpress
kubectl get pvc,pods,svc

Vérifiez les points de câblage :

# Les deux PVC sont Bound
kubectl get pvc

# Chaque Service a bien un Pod derrière lui
kubectl get endpointslices

# Les fichiers WordPress ont été copiés dans le volume
kubectl exec deploy/wordpress -- ls /var/www/html

Test rapide en ligne de commande :

curl -sI http://localhost:30090 | head -n 3

Une réponse 302 Found vers /wp-admin/install.php signifie que WordPress fonctionne et est connecté à la base.

Si vous obtenez « Error establishing a database connection », voir l'Annexe A.

Partie 6 : Installer WordPress et tester la persistance

6.1 Installation

Ouvrez http://localhost:30090 dans votre navigateur et suivez l'assistant :

  1. choisissez la langue ;
  2. renseignez le titre du site, un identifiant administrateur et un mot de passe ;
  3. connectez-vous au tableau de bord.

Puis, pour avoir des données à protéger :

  1. créez un article intitulé « Test de persistance Kubernetes » et publiez-le ;
  2. dans Médias > Ajouter, téléversez une image quelconque.

6.2 Où sont stockées les données ?

L'article est dans la base MariaDB :

kubectl exec -it deploy/mariadb -- mariadb -u wpuser -p'WpUserTP-2026!' wordpress \
  -e "SELECT ID, post_title, post_status FROM wp_posts WHERE post_type='post';"

L'image est dans le volume WordPress :

kubectl exec deploy/wordpress -- find /var/www/html/wp-content/uploads -type f

6.3 Expérience 1 : supprimer les Pods

kubectl delete pod -l app=wordpress
kubectl delete pod -l app=mariadb
kubectl get pods -w      # attendez que les deux Pods soient Running, puis Ctrl+C

Rechargez http://localhost:30090 : l'article et l'image sont toujours là. Les Pods sont neufs, mais ils ont remonté les mêmes volumes.

6.4 Expérience 2 : supprimer les Deployments

kubectl delete deployment wordpress mariadb
kubectl get pods
kubectl get pvc

Plus aucun Pod ne tourne, mais les PVC sont toujours Bound. Recréez les Deployments :

kubectl apply -f mariadb.yaml -f wordpress.yaml
kubectl rollout status deployment mariadb
kubectl rollout status deployment wordpress

Rechargez la page : tout est intact. Le cycle de vie des données est indépendant de celui des applications.

6.5 Expérience 3 : que se passe-t-il sans la base ?

kubectl scale deployment mariadb --replicas=0
curl -s http://localhost:30090 | grep -o "Error establishing a database connection"
kubectl scale deployment mariadb --replicas=1
kubectl rollout status deployment mariadb
curl -sI http://localhost:30090 | head -n 1

Le Service mariadb existe toujours mais n'a plus aucun Pod derrière lui : WordPress ne peut plus joindre la base. Dès que le Pod revient, le site fonctionne de nouveau, sans rien reconfigurer.

6.6 Question de réflexion : peut-on passer WordPress à 3 réplicas ?

Essayez :

kubectl scale deployment wordpress --replicas=3
kubectl get pods -l app=wordpress -o wide

Sur minikube, cela fonctionne en apparence car il n'y a qu'un seul nœud : les trois Pods partagent le même dossier. Sur un vrai cluster à plusieurs nœuds, un volume ReadWriteOnce ne peut être monté que sur un nœud : les Pods placés ailleurs resteraient bloqués en ContainerCreating. Pour faire tourner WordPress sur plusieurs nœuds, il faut un stockage ReadWriteMany (NFS, CephFS...) ou stocker les médias dans un stockage objet (S3).

Revenez à un réplica :

kubectl scale deployment wordpress --replicas=1

✅ Point de contrôle 6

  1. Où sont stockés les articles ? Où sont stockées les images téléversées ?
  2. Après kubectl delete deployment wordpress mariadb, pourquoi les données sont-elles toujours là ?
  3. Pourquoi augmenter les réplicas de WordPress pose-t-il problème sur un cluster multi-nœuds ?

Partie 7 : Exercice, ajouter phpMyAdmin en NodePort

Déployez phpMyAdmin pour administrer la base depuis le navigateur.

Consignes :

  1. Créez phpmyadmin.yaml contenant un Deployment et un Service.
  2. Image : phpmyadmin:5.2, label app: phpmyadmin, 1 réplica, port conteneur 80.
  3. phpMyAdmin doit se connecter à la base via la variable d'environnement PMA_HOST. Quelle valeur lui donner ?
  4. phpMyAdmin n'a pas besoin de volume : il ne stocke aucune donnée. Pourquoi ?
  5. Service de type NodePort exposé sur le port 30091.
  6. Ouvrez http://localhost:30091, connectez-vous avec l'utilisateur wpuser et retrouvez votre article dans la table wp_posts.

Bonus :

  • Listez tous les Services du namespace et, pour chacun, indiquez s'il est accessible depuis l'extérieur.

Affichez le NodePort de chaque Service avec :

kubectl get svc -o custom-columns=NOM:.metadata.name,TYPE:.spec.type,NODEPORT:.spec.ports[0].nodePort

Partie 8 : Migrer MariaDB vers un StatefulSet

8.1 Notion

Jusqu'ici, MariaDB tourne dans un Deployment. Ça fonctionne avec un seul réplica, mais le Deployment est conçu pour des applications sans état dont tous les Pods sont interchangeables : noms aléatoires, un seul PVC partagé par tous les Pods, démarrage de tous les Pods en même temps.

Une base de données a d'autres besoins : chaque instance a ses propres données, doit garder la même identité après un redémarrage, et les instances doivent souvent démarrer dans un ordre précis (le primaire avant les réplicas). C'est le rôle du StatefulSet.

Deployment StatefulSet
Nom des Pods aléatoire : mariadb-7c9f8d-x2k4p stable et numéroté : mariadb-0, mariadb-1...
Après recréation nouveau nom, nouvelle identité même nom, même volume
Stockage un PVC déclaré à part, partagé par tous les Pods un PVC par Pod, créé automatiquement (volumeClaimTemplates)
Démarrage / arrêt tous les Pods en parallèle dans l'ordre : 0, 1, 2 au démarrage, 2, 1, 0 à l'arrêt
DNS un nom pour le Service un nom par Pod via un Service headless : mariadb-0.mariadb-headless
Usage type serveurs web, API, phpMyAdmin bases de données, Kafka, Elasticsearch, etcd

8.2 Le Service headless

Un StatefulSet a besoin d'un Service headless : un Service sans IP virtuelle (clusterIP: None). Au lieu de répartir la charge, il publie dans le DNS une entrée par Pod. C'est ce qui permet de joindre précisément mariadb-0 plutôt que « n'importe quel Pod MariaDB ».

Service "mariadb" (ClusterIP)            Service "mariadb-headless" (clusterIP: None)
mariadb ──► IP virtuelle ──► un Pod      mariadb-0.mariadb-headless ──► Pod mariadb-0
            (répartition de charge)      mariadb-1.mariadb-headless ──► Pod mariadb-1

On gardera les deux : le Service mariadb pour WordPress (rien à changer côté WordPress), et le Service headless pour l'identité des Pods.

8.3 Sauvegarder la base avant la migration

Le StatefulSet va créer son propre PVC. Les données actuelles, qui sont dans mariadb-data, ne seront pas reprises automatiquement. On passe donc par une sauvegarde et une restauration, comme on le ferait en production.

Pendant la migration, le site WordPress sera indisponible quelques minutes. En production, on planifie ce type d'opération dans une fenêtre de maintenance.

Exportez la base dans un fichier sur votre machine :

kubectl exec deploy/mariadb -- sh -c 'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" wordpress' > backup-wordpress.sql
ls -lh backup-wordpress.sql
grep -c "Test de persistance Kubernetes" backup-wordpress.sql

La dernière commande doit renvoyer au moins 1 : votre article est bien dans la sauvegarde.

$MARIADB_ROOT_PASSWORD est évalué à l'intérieur du conteneur (grâce aux guillemets simples), où la variable a été injectée depuis le Secret. Le mot de passe n'apparaît pas dans votre historique de commandes.

8.4 Le manifeste

Créez mariadb-statefulset.yaml :

# 1) Service headless : un nom DNS par Pod
apiVersion: v1
kind: Service
metadata:
  name: mariadb-headless
  labels:
    app: mariadb
spec:
  clusterIP: None                  # c'est ce qui rend le Service "headless"
  selector:
    app: mariadb
  ports:
    - port: 3306
      targetPort: 3306
---
# 2) Service utilisé par WordPress (identique à celui de la partie 4)
apiVersion: v1
kind: Service
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  type: ClusterIP
  selector:
    app: mariadb
  ports:
    - port: 3306
      targetPort: 3306
---
# 3) Le StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  serviceName: mariadb-headless    # Service headless qui donne leur nom DNS aux Pods
  replicas: 1
  selector:
    matchLabels:
      app: mariadb
  template:                        # identique au template du Deployment, sans la partie volumes
    metadata:
      labels:
        app: mariadb
    spec:
      containers:
        - name: mariadb
          image: mariadb:11.4
          ports:
            - containerPort: 3306
          env:
            - name: MARIADB_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: root-password
            - name: MARIADB_DATABASE
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: database
            - name: MARIADB_USER
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: user
            - name: MARIADB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: wordpress-db
                  key: password
          volumeMounts:
            - name: data                   # correspond au nom du volumeClaimTemplate
              mountPath: /var/lib/mysql
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
            limits:
              memory: 512Mi
  volumeClaimTemplates:            # modèle de PVC : un PVC sera créé pour CHAQUE Pod
    - metadata:
        name: data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
Explications :serviceName relie le StatefulSet à son Service headless. Chaque Pod devient joignable par <pod>.<service-headless>, par exemple mariadb-0.mariadb-headless.Il n'y a plus de bloc volumes ni de PVC déclaré à part. volumeClaimTemplates est un modèle : pour chaque Pod, Kubernetes crée un PVC nommé <nom du template>-<nom du Pod>, donc data-mariadb-0, data-mariadb-1...Pas de strategy: Recreate : un StatefulSet ne lance jamais deux Pods portant le même nom. mariadb-0 est toujours arrêté avant que son remplaçant ne démarre.Les Pods gardent le label app: mariadb. Le Service mariadb utilisé par WordPress les trouvera donc sans modification : un Service est lié à des labels, pas à un Deployment ou un StatefulSet.

8.5 Remplacer le Deployment par le StatefulSet

Supprimez le Deployment (le Service mariadb et le PVC mariadb-data sont conservés) :

kubectl delete deployment mariadb

Déployez le StatefulSet et observez :

kubectl apply -f mariadb-statefulset.yaml
kubectl get pods -l app=mariadb -w       # attendez mariadb-0 en Running, puis Ctrl+C
kubectl get statefulset,pvc,svc

Observez :

  • le Pod s'appelle mariadb-0, sans suffixe aléatoire ;
  • un nouveau PVC data-mariadb-0 a été créé automatiquement, à côté de l'ancien mariadb-data ;
  • le Service mariadb-headless affiche None dans la colonne CLUSTER-IP.

Pour ne pas recréer l'ancien Deployment par erreur, renommez l'ancien fichier :

mv mariadb.yaml mariadb-deployment.old.yaml

8.6 Restaurer les données

La base wordpress du nouveau Pod est vide (créée par les variables MARIADB_*). Rechargez http://localhost:30090 : WordPress propose à nouveau son installation. Ne la faites pas, restaurez plutôt la sauvegarde :

kubectl logs mariadb-0 | grep -i "ready for connections"
kubectl exec -i mariadb-0 -- sh -c 'mariadb -u root -p"$MARIADB_ROOT_PASSWORD" wordpress' < backup-wordpress.sql
L'option -i (sans -t) transmet le contenu du fichier local à l'entrée standard de la commande dans le conteneur.

Vérifiez :

kubectl exec mariadb-0 -- sh -c 'mariadb -u wpuser -p"$MARIADB_PASSWORD" wordpress -e "SELECT post_title FROM wp_posts WHERE post_type=\"post\";"'
curl -sI http://localhost:30090 | head -n 1

Rechargez le site : l'article et l'image sont de retour. WordPress n'a pas été modifié ni redémarré, il a simplement retrouvé une base derrière le Service mariadb.

Une fois la migration validée, supprimez l'ancien volume :

kubectl delete pvc mariadb-data

8.7 Expérience 1 : une identité stable

kubectl delete pod mariadb-0
kubectl get pods -l app=mariadb -w       # Ctrl+C quand mariadb-0 est Running
kubectl get pvc

Le Pod recréé porte le même nom mariadb-0 et a remonté le même PVC data-mariadb-0. Avec un Deployment, le nom aurait changé.

Testez le DNS par Pod :

kubectl run dnstest --rm -it --image=busybox:1.36 --restart=Never -- nslookup mariadb-0.mariadb-headless

8.8 Expérience 2 : mise à l'échelle et piège de la réplication

Surveillez les Pods dans un second terminal :

kubectl get pods -l app=mariadb -w

Passez à 3 réplicas :

kubectl scale statefulset mariadb --replicas=3
kubectl get pvc

Observez :

  • les Pods démarrent l'un après l'autre : mariadb-1 n'est créé qu'une fois mariadb-0 prêt, puis mariadb-2 ;
  • chaque Pod a son propre PVC : data-mariadb-1, data-mariadb-2.

Maintenant, interrogez WordPress plusieurs fois :

for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://localhost:30090/; done

Une partie des réponses redirige vers /wp-admin/install.php. Ne cliquez sur rien dans le navigateur.

Explication : mariadb-1 et mariadb-2 sont des bases indépendantes et vides. Le StatefulSet fournit l'identité et le stockage de chaque instance, mais il ne réplique pas les données. Le Service mariadb répartit les connexions entre les trois Pods : quand WordPress tombe sur une base vide, il croit qu'il n'est pas installé.

La réplication est la responsabilité de l'application (réplication native MariaDB, Galera...), en général mise en place par un opérateur Kubernetes (par exemple mariadb-operator) ou un chart Helm. Le StatefulSet n'en est que la fondation.

Revenez à un réplica :

kubectl scale statefulset mariadb --replicas=1
kubectl get pods -l app=mariadb
kubectl get pvc

Les Pods s'arrêtent dans l'ordre inverse (mariadb-2 puis mariadb-1), mais leurs PVC sont conservés. C'est un comportement volontaire : réduire le nombre de réplicas ne doit jamais détruire de données par accident. Si vous remontez à 3, mariadb-1 retrouvera exactement son volume. Supprimez-les manuellement :

kubectl delete pvc data-mariadb-1 data-mariadb-2
Ce comportement se règle avec le champ persistentVolumeClaimRetentionPolicy du StatefulSet (whenScaled et whenDeleted, valeur Retain par défaut).

8.9 Exercice : cibler uniquement le primaire

Dans une vraie architecture primaire/réplicas, les écritures ne doivent aller qu'au primaire, mariadb-0.

Kubernetes ajoute automatiquement à chaque Pod d'un StatefulSet un label contenant son nom :

kubectl get pod mariadb-0 --show-labels

Consignes :

  1. Repérez le label qui contient le nom du Pod.
  2. Modifiez le Service mariadb dans mariadb-statefulset.yaml pour qu'il ne cible que mariadb-0.
  3. Appliquez, puis passez le StatefulSet à 3 réplicas.
  4. Relancez la boucle curl : toutes les réponses doivent maintenant être correctes (200 ou redirection vers la page d'accueil, jamais vers install.php).
  5. Revenez à 1 réplica et supprimez les PVC en trop.

✅ Point de contrôle 8

  1. Citez trois différences entre un Deployment et un StatefulSet.
  2. À quoi sert un Service headless ?
  3. Quel sera le nom du PVC du Pod mariadb-2 ?
  4. Après kubectl scale statefulset mariadb --replicas=3, pourquoi WordPress propose-t-il parfois une nouvelle installation ?
  5. Pourquoi les PVC ne sont-ils pas supprimés quand on réduit le nombre de réplicas ?

Partie 9 : Nettoyage

Supprimer tout le TP en une commande :

kubectl delete namespace wordpress
kubectl get pv

La suppression du namespace supprime les Deployments, le StatefulSet, les Services, le Secret et tous les PVC (y compris data-mariadb-0). Avec la politique Delete, les PV et les données sont également détruits.

Rétablissez le namespace par défaut de kubectl :

kubectl config set-context --current --namespace=default

Arrêter ou supprimer le cluster :

minikube stop
minikube delete

Annexe A : Dépannage

Symptôme Cause probable Vérification / solution
PVC en Pending Pas de StorageClass par défaut kubectl get storageclass, minikube addons enable storage-provisioner
« Error establishing a database connection » MariaDB pas encore prête, ou identifiants différents kubectl logs deploy/mariadb, vérifier ready for connections et les clés du Secret
MariaDB refuse wpuser après modification du Secret Les variables MARIADB_* ne sont lues qu'au premier démarrage Supprimer Deployment et PVC mariadb-data puis réappliquer (perte des données)
Pod MariaDB en CrashLoopBackOff Mémoire insuffisante (OOMKilled) ou volume corrompu kubectl describe pod -l app=mariadb, kubectl logs deploy/mariadb --previous
localhost:30090 ne répond pas Cluster démarré sans --ports minikube delete puis redémarrer avec --ports=30090:30090,30091:30091
Redirection vers une mauvaise adresse WordPress installé via une autre URL que localhost:30090 Supprimer le namespace et réinstaller en utilisant uniquement http://localhost:30090
Invalid value: 30090: provided port is already allocated Un autre Service utilise déjà ce NodePort `kubectl get svc -A
The Service ... nodePort: Invalid value Port hors de la plage 30000-32767 Choisir un port dans la plage
WordPress propose l'installation après la migration Base du StatefulSet vide, sauvegarde pas encore restaurée Refaire l'étape 8.6 sans lancer l'installation
WordPress propose l'installation par intermittence Plusieurs réplicas MariaDB non répliqués derrière le Service kubectl scale statefulset mariadb --replicas=1 ou exercice 8.9
mariadb-1 n'est jamais créé mariadb-0 n'est pas prêt, le StatefulSet attend kubectl describe pod mariadb-0, kubectl logs mariadb-0

Commandes de diagnostic utiles :

kubectl get all,pvc
kubectl get events --sort-by=.lastTimestamp
kubectl describe pod -l app=wordpress
kubectl logs deploy/wordpress

Annexe B : Corrigés

Point de contrôle 2

  1. Le système de fichiers d'un conteneur est créé à partir de l'image au démarrage et détruit avec lui. Tout ce qui y est écrit disparaît avec le Pod.
  2. Le PVC est la demande (taille, mode d'accès) faite par l'application. Le PV est la ressource de stockage réelle qui satisfait cette demande. Un PVC est lié à un seul PV.
  3. Le PV associé est supprimé et les données avec lui.

Point de contrôle 4

  1. Seul WordPress, à l'intérieur du cluster, a besoin de joindre la base. L'exposer à l'extérieur augmenterait inutilement la surface d'attaque.
  2. RollingUpdate ferait tourner brièvement deux instances MariaDB sur les mêmes fichiers, avec un risque de corruption. Recreate arrête l'ancien Pod avant de démarrer le nouveau. C'est aussi nécessaire avec un volume ReadWriteOnce sur un cluster multi-nœuds.

Point de contrôle 6

  1. Les articles sont dans la base MariaDB (table wp_posts), donc dans le PVC mariadb-data. Les images sont dans /var/www/html/wp-content/uploads, donc dans le PVC wordpress-data.
  2. Les données sont dans les PVC, qui sont des objets indépendants des Deployments. Supprimer un Deployment ne supprime pas les PVC qu'il utilise.
  3. Un volume ReadWriteOnce ne peut être monté que sur un seul nœud. Les réplicas planifiés sur d'autres nœuds ne pourraient pas démarrer.

Exercice : phpmyadmin.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: phpmyadmin
  labels:
    app: phpmyadmin
spec:
  replicas: 1
  selector:
    matchLabels:
      app: phpmyadmin
  template:
    metadata:
      labels:
        app: phpmyadmin
    spec:
      containers:
        - name: phpmyadmin
          image: phpmyadmin:5.2
          ports:
            - containerPort: 80
          env:
            - name: PMA_HOST
              value: mariadb          # nom du Service ClusterIP de la base
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: phpmyadmin
  labels:
    app: phpmyadmin
spec:
  type: NodePort
  selector:
    app: phpmyadmin
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30091
kubectl apply -f phpmyadmin.yaml
kubectl rollout status deployment phpmyadmin
curl -sI http://localhost:30091 | head -n 1

Réponse à la question 4 : phpMyAdmin est une application sans état (stateless). Il se contente de lire et d'écrire dans la base MariaDB, qui possède déjà son propre volume. Supprimer son Pod ne fait perdre aucune donnée.

Bonus : wordpress (NodePort 30090) et phpmyadmin (NodePort 30091) sont accessibles depuis l'extérieur. mariadb (ClusterIP) ne l'est pas.

Point de contrôle 8

  1. Noms de Pods stables et numérotés (contre aléatoires) ; un PVC par Pod créé par volumeClaimTemplates (contre un PVC partagé déclaré à part) ; démarrage et arrêt ordonnés (contre parallèles) ; un nom DNS par Pod via le Service headless.
  2. Il publie dans le DNS une entrée par Pod au lieu d'une IP virtuelle unique. Il permet de joindre un Pod précis, par exemple mariadb-0.mariadb-headless, ce qui est indispensable pour désigner un primaire ou configurer une réplication.
  3. data-mariadb-2 : nom du volumeClaimTemplate suivi du nom du Pod.
  4. mariadb-1 et mariadb-2 sont des bases indépendantes et vides. Le StatefulSet ne réplique pas les données, et le Service mariadb répartit les connexions entre les trois Pods.
  5. Pour éviter toute perte de données accidentelle. Un Pod recréé plus tard avec le même numéro retrouve son volume. Le comportement se règle avec persistentVolumeClaimRetentionPolicy.

Exercice 8.9 : cibler uniquement le primaire

Le label est statefulset.kubernetes.io/pod-name=mariadb-0 (Kubernetes récents ajoutent aussi apps.kubernetes.io/pod-index=0).

Service mariadb modifié dans mariadb-statefulset.yaml :

apiVersion: v1
kind: Service
metadata:
  name: mariadb
  labels:
    app: mariadb
spec:
  type: ClusterIP
  selector:
    app: mariadb
    statefulset.kubernetes.io/pod-name: mariadb-0   # uniquement le primaire
  ports:
    - port: 3306
      targetPort: 3306
kubectl apply -f mariadb-statefulset.yaml
kubectl scale statefulset mariadb --replicas=3
kubectl get endpointslices -l kubernetes.io/service-name=mariadb      # une seule IP : celle de mariadb-0
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" http://localhost:30090/; done
kubectl scale statefulset mariadb --replicas=1
kubectl delete pvc data-mariadb-1 data-mariadb-2

Le selector d'un Service combine les labels avec un ET logique : seul un Pod portant les deux labels est ciblé.


TP Helm : déployer Supabase sur Kubernetes

Durée estimée : 3 h à 3 h 30 Niveau : débutant Helm, suite des TP Kubernetes (Pod, Deployment, Service, volumes, StatefulSet) Prérequis : minikube et kubectl installés, notions de Deployment, Service, Secret, PVC et StatefulSet

Objectifs

À la fin de ce TP, vous saurez :

  1. expliquer ce que sont un chart, une release, un dépôt et les values Helm ;
  2. explorer un chart avant de l'installer (helm show, helm template) ;
  3. comprendre comment un template transforme des values en manifestes Kubernetes ;
  4. écrire vos propres fichiers de values et générer des secrets propres ;
  5. installer, inspecter, mettre à jour et revenir en arrière sur une release ;
  6. éviter les deux pièges classiques de helm upgrade ;
  7. désinstaller une application sans perdre ses données.

L'application support est Supabase, une plateforme backend open source construite autour de PostgreSQL (API REST automatique, authentification, stockage de fichiers, temps réel). Elle comporte une dizaine de composants : c'est exactement le type d'application qu'on ne déploie pas à la main avec des fichiers YAML, et qui justifie Helm.

Sommaire


Partie 0 : Préparation

0.1 Ressources nécessaires

Supabase est une pile complète : prévoyez 4 CPU et 6 Go de RAM libres pour le cluster, et une bonne connexion (environ 3 Go d'images à télécharger au premier déploiement).

0.2 Créer un cluster dédié

On utilise un profil minikube séparé pour ne pas mélanger ce TP avec les précédents :

minikube start -p supabase --driver=docker --cpus=4 --memory=6144
minikube addons enable ingress -p supabase
kubectl get nodes
Addon ingress : le chart Supabase expose l'application par un Ingress, un objet Kubernetes qui route le trafic HTTP selon le nom de domaine demandé (ici supabase.local). L'addon installe le contrôleur NGINX qui applique ces règles. Vous n'avez rien à configurer vous-même : le chart crée l'Ingress.

Vérifiez que le contrôleur est prêt :

kubectl get pods -n ingress-nginx

0.3 Faire pointer supabase.local vers le cluster

Votre navigateur doit résoudre le nom supabase.local. On l'ajoute au fichier hosts de votre machine.

Linux :

echo "$(minikube ip -p supabase) supabase.local" | sudo tee -a /etc/hosts

macOS :

echo "127.0.0.1 supabase.local" | sudo tee -a /etc/hosts

Windows : ouvrez un éditeur en administrateur, éditez C:\Windows\System32\drivers\etc\hosts et ajoutez la ligne 127.0.0.1 supabase.local.

macOS et Windows uniquement : avec le driver Docker, l'IP du cluster n'est pas joignable depuis votre machine. Il faudra lancer minikube tunnel -p supabase dans un terminal dédié au moment d'utiliser Supabase (partie 6). Cette commande demande votre mot de passe administrateur car elle ouvre le port 80.

0.4 Installer Helm

Linux :

curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

macOS :

brew install helm

Windows :

winget install Helm.Helm

Vérifiez :

helm version
Helm n'installe rien dans le cluster. C'est un simple client qui utilise la même configuration que kubectl (~/.kube/config) : il agit sur le cluster et le namespace du contexte courant.

0.5 Namespace et dossier de travail

kubectl create namespace supabase
kubectl config set-context --current --namespace=supabase
mkdir -p ~/tp-helm-supabase && cd ~/tp-helm-supabase

Dans ce TP, on écrira quand même -n supabase sur les commandes helm. C'est une bonne habitude : une release installée dans le mauvais namespace est une erreur fréquente et pénible à corriger.


Partie 1 : Les notions de Helm

1.1 Le problème

Dans les TP précédents, WordPress demandait déjà une dizaine d'objets (Secret, PVC, Deployments, Services). Supabase en demande plus de 50 : Deployments, StatefulSet, Services, Secrets, ConfigMaps, PVC, Ingress, ServiceAccounts. Les écrire, les maintenir cohérents et les faire évoluer à la main serait long et source d'erreurs.

1.2 Le vocabulaire

Terme Définition Analogie
Chart Paquet contenant des templates de manifestes et leurs valeurs par défaut un paquet .deb ou .rpm
Dépôt (repository) Serveur HTTP (ou registre OCI) qui publie des charts un dépôt APT
Values Paramètres qui personnalisent un chart (values.yaml, -f, --set) un fichier de configuration
Release Une installation d'un chart dans un cluster, avec un nom un paquet installé
Révision Une version d'une release : chaque install, upgrade ou rollback en crée une un commit
   Chart (templates + values.yaml par défaut)
                 +
   Vos values (-f mes-values.yaml, --set ...)
                 │
                 ▼
         helm install / upgrade
                 │  génère les manifestes
                 ▼
   Manifestes Kubernetes ──► API Kubernetes
                 │
                 ▼
   Release "demo", révision 1, 2, 3...
   (historique stocké dans des Secrets du namespace)

1.3 Le cycle de vie d'une release

Commande Effet
helm install crée la release (révision 1)
helm upgrade applique une nouvelle configuration ou une nouvelle version du chart (révision +1)
helm rollback revient à la configuration d'une révision précédente (révision +1)
helm uninstall supprime les ressources de la release

Partie 2 : Explorer un chart sans l'installer

2.1 Ajouter le dépôt

helm repo add supabase https://supabase-community.github.io/supabase-kubernetes
helm repo update
helm repo list
helm repo update télécharge l'index du dépôt (la liste des charts et de leurs versions). Pensez à le relancer avant chaque mise à jour : sinon Helm ne voit pas les nouvelles versions.

2.2 Rechercher un chart et ses versions

helm search repo supabase
helm search repo supabase/supabase --versions

Ce TP a été écrit avec la version 0.8.0 du chart. On la fixera explicitement dans toutes les commandes avec --version 0.8.0.

Pourquoi fixer la version ? Sans --version, Helm prend la dernière version publiée. Deux personnes qui lancent la même commande à une semaine d'écart peuvent obtenir des déploiements différents. En production, la version du chart se fixe toujours, comme une version de dépendance dans un projet.

2.3 Lire la documentation embarquée

helm show chart  supabase/supabase --version 0.8.0
helm show readme supabase/supabase --version 0.8.0 | less

helm show chart affiche le fichier Chart.yaml : nom, version, description, dépendances éventuelles.

2.4 Lire les values par défaut

C'est la commande la plus importante avant toute installation :

helm show values supabase/supabase --version 0.8.0 > values-defaut.yaml
wc -l values-defaut.yaml
grep -n "^[a-zA-Z]" values-defaut.yaml

La dernière commande liste les sections principales. Voici leur rôle :

Section Contenu
secret mots de passe, clés JWT, identifiants du tableau de bord
deployment pour chaque composant : activé ou non, réplicas, ressources, affinité...
image image et tag de chaque composant
environment variables d'environnement de chaque composant
persistence taille et options des PVC
service type et port des Services
ingress exposition HTTP par nom de domaine

✅ Point de contrôle 2

En lisant values-defaut.yaml (utilisez grep -n et less), répondez :

  1. Quels composants sont désactivés par défaut ? (cherchez enabled: false dans la section deployment)
  2. Quelle image et quel tag sont utilisés pour la base de données ?
  3. Quelle taille de disque est demandée par défaut pour la base de données ?
  4. Quel est le mot de passe par défaut du tableau de bord ? Que vous inspire le fait qu'il soit écrit dans un fichier public ?
  5. Quel nom de domaine est configuré par défaut dans la section ingress ?

Partie 3 : Comprendre le templating

3.1 Regarder à l'intérieur du chart

Téléchargez et décompressez le chart pour l'examiner :

helm pull supabase/supabase --version 0.8.0 --untar
ls supabase
ls supabase/templates
ls supabase/templates/kong

Structure d'un chart :

Élément Rôle
Chart.yaml identité du chart (nom, version)
values.yaml valeurs par défaut (ce qu'affiche helm show values)
templates/ manifestes Kubernetes avec des trous à remplir
templates/_helpers.tpl fonctions réutilisables (noms, labels communs)
templates/NOTES.txt message affiché après l'installation

3.2 Lire un template

cat supabase/templates/kong/service.yaml
{{- if .Values.deployment.kong.enabled -}}
apiVersion: v1
kind: Service
metadata:
  name: {{ include "supabase.kong.fullname" . }}
  labels:
    {{- include "supabase.labels" . | nindent 4 }}
    app.kubernetes.io/component: kong
spec:
  type: {{ .Values.service.kong.type }}
  ports:
    - port: {{ .Values.service.kong.port }}
      targetPort: 8000
      protocol: TCP
      name: http
  selector:
    {{- include "supabase.kong.selectorLabels" . | nindent 4 }}
{{- end }}
Lecture :Tout ce qui est entre {{ }} est calculé par Helm. Le reste est recopié tel quel..Values.service.kong.type va chercher la valeur service → kong → type dans les values. C'est le lien direct entre values.yaml et le manifeste final.{{- if ... }} ... {{- end }} : le Service n'est généré que si le composant Kong est activé.include "supabase.kong.fullname" appelle une fonction de _helpers.tpl qui construit le nom <release>-supabase-kong.

Repérez la valeur correspondante dans les values par défaut :

grep -n -A3 "^  kong:" values-defaut.yaml | grep -A3 -B1 "type:"

3.3 Générer les manifestes sans rien installer

helm template produit les manifestes finaux localement, sans contacter le cluster :

helm template demo supabase/supabase --version 0.8.0 -n supabase > rendu-defaut.yaml
grep "^kind:" rendu-defaut.yaml | sort | uniq -c

Vous obtenez un inventaire de tout ce que le chart créerait : une dizaine de Deployments, un StatefulSet, des Secrets, des PVC, un Ingress...

Les objets Job de cet inventaire sont des tests Helm (voir partie 5). Ils ne sont pas créés à l'installation. Pour les exclure : --skip-tests.

3.4 Observer l'effet d'une value

Affichez un seul fichier du rendu, avec la valeur par défaut puis avec une valeur modifiée :

helm template demo supabase/supabase --version 0.8.0 \
  --show-only templates/kong/service.yaml

helm template demo supabase/supabase --version 0.8.0 \
  --show-only templates/kong/service.yaml \
  --set service.kong.type=NodePort

Seule la ligne type: change. C'est tout le principe de Helm : on modifie une valeur, le chart régénère des manifestes cohérents.

Autre exemple, sur la taille du disque de la base :

helm template demo supabase/supabase --version 0.8.0 --skip-tests --set persistence.db.size=2Gi \
  | grep -B2 -A12 "name: demo-supabase-db$" | grep -E "kind:|name:|storage:"

✅ Point de contrôle 3

  1. Quelle différence entre helm template et helm install ?
  2. Dans un template, à quoi correspond {{ .Values.persistence.db.size }} ?
  3. Pourquoi le nom des ressources commence-t-il par demo- ?

Partie 4 : Préparer sa configuration

4.1 Le principe : ne jamais modifier le chart

On ne touche ni au dossier supabase/ téléchargé ni au values-defaut.yaml. On écrit un fichier qui contient uniquement les valeurs qu'on change. Helm fusionne ce fichier avec les valeurs par défaut du chart.

On va séparer la configuration en deux fichiers :

Fichier Contenu Versionné dans Git ?
values-tp.yaml configuration technique (tailles, réplicas, options) oui
values-secrets.yaml mots de passe et clés non, jamais

4.2 Le fichier de configuration

Créez values-tp.yaml :

# Configuration du TP : uniquement ce qui diffère des valeurs par défaut du chart
persistence:
  db:
    size: 2Gi                              # 5 Gi par défaut, inutile pour un TP
    annotations:
      helm.sh/resource-policy: keep        # conserver ce PVC même après helm uninstall
  pgsodium:
    annotations:
      helm.sh/resource-policy: keep        # clé de chiffrement liée aux données de la base
L'annotation helm.sh/resource-policy: keep demande à Helm de ne pas supprimer cette ressource lors d'un helm uninstall. Sans elle, désinstaller la release supprimerait le PVC, donc la base de données. On le vérifiera en partie 8.

4.3 Générer les secrets

Les valeurs par défaut du chart contiennent des mots de passe et des clés publiés sur Internet. Il faut les remplacer. Supabase a besoin en particulier de trois éléments liés entre eux :

  • un secret JWT : la clé qui signe tous les jetons d'accès ;
  • une clé anon : un jeton JWT signé avec ce secret, utilisé par les clients publics (navigateur, application mobile) ;
  • une clé service_role : un jeton JWT signé avec ce secret, qui contourne les règles de sécurité. Il ne doit jamais sortir du serveur.

Créez le script gen-secrets.sh :

#!/usr/bin/env bash
# Génère values-secrets.yaml avec des secrets aléatoires pour Supabase
set -euo pipefail

b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }

JWT_SECRET=$(openssl rand -hex 32)
IAT=$(date +%s)
EXP=$((IAT + 5*365*24*3600))          # validité : 5 ans
HEADER=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url)

# Signe un JWT HS256 pour le rôle passé en argument
sign() {
  local payload sig
  payload=$(printf '{"role":"%s","iss":"supabase","iat":%d,"exp":%d}' "$1" "$IAT" "$EXP" | b64url)
  sig=$(printf '%s.%s' "$HEADER" "$payload" | openssl dgst -sha256 -hmac "$JWT_SECRET" -binary | b64url)
  printf '%s.%s.%s' "$HEADER" "$payload" "$sig"
}

cat > values-secrets.yaml <<YAML
secret:
  jwt:
    secret: "${JWT_SECRET}"
    anonKey: "$(sign anon)"
    serviceKey: "$(sign service_role)"
  db:
    password: "$(openssl rand -hex 24)"
    database: "postgres"
  dashboard:
    username: "admin"
    password: "$(openssl rand -hex 12)"
  realtime:
    secretKeyBase: "$(openssl rand -base64 64 | tr -d '\n')"
    dbEncKey: "$(openssl rand -hex 8)"
  meta:
    cryptoKey: "$(openssl rand -hex 32)"
YAML

chmod 600 values-secrets.yaml
echo "values-secrets.yaml généré"

Exécutez-le une seule fois :

bash gen-secrets.sh
cat values-secrets.yaml
Attention : ne relancez pas ce script une fois Supabase installé. La base de données garde le mot de passe choisi au premier démarrage : de nouveaux secrets la rendraient inaccessible aux autres composants.

Si vous utilisez Git : echo "values-secrets.yaml" >> .gitignore.

Curiosité : décodez la partie centrale de la clé anon pour voir ce qu'elle contient :

grep anonKey values-secrets.yaml | cut -d'"' -f2 | cut -d. -f2 | tr '_-' '/+' | base64 -d 2>/dev/null; echo

Un JWT n'est pas chiffré : son contenu est lisible par tous. Seule la signature (3e partie) garantit qu'il n'a pas été falsifié.

4.4 L'ordre de priorité des values

Quand plusieurs sources de values sont données, la dernière gagne :

values.yaml du chart  <  -f fichier1.yaml  <  -f fichier2.yaml  <  --set cle=valeur
   (priorité faible)                                                (priorité forte)

Vérifiez que vos valeurs sont bien prises en compte :

helm template demo supabase/supabase --version 0.8.0 -n supabase --skip-tests \
  -f values-tp.yaml -f values-secrets.yaml > rendu-tp.yaml

# Le PVC de la base : 2Gi et annotation keep
grep -B3 -A14 "name: demo-supabase-db$" rendu-tp.yaml | grep -E "kind:|resource-policy|storage:"

# Le Secret du tableau de bord contient "admin" encodé en base64
grep -A12 "name: demo-supabase-dashboard$" rendu-tp.yaml | grep username
echo YWRtaW4= | base64 -d; echo

✅ Point de contrôle 4

  1. Pourquoi ne modifie-t-on pas directement le values.yaml du chart ?
  2. Si values-tp.yaml indique size: 2Gi et qu'on ajoute --set persistence.db.size=3Gi, quelle taille sera utilisée ?
  3. Pourquoi la clé service_role ne doit-elle jamais être utilisée dans un navigateur ?

Partie 5 : Installer et inspecter la release

5.1 Simuler l'installation

--dry-run envoie tout au serveur pour validation, sans rien créer :

helm install demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml --dry-run | head -n 20

5.2 Installer

helm install demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml

Helm rend la main immédiatement et affiche le contenu de NOTES.txt. Les Pods, eux, démarrent en arrière-plan. Suivez-les :

kubectl get pods -l app.kubernetes.io/instance=demo -w

Le premier déploiement prend 5 à 10 minutes (téléchargement des images, initialisation de la base). Il est normal de voir quelques redémarrages (RESTARTS) sur auth, rest, realtime ou storage : ils attendent que PostgreSQL soit prêt. Attendez que tout soit Running et 1/1, puis Ctrl+C.

Pendant l'attente, lisez les logs de la base : kubectl logs statefulset/demo-supabase-db -f

5.3 Inspecter la release

helm list -n supabase
helm status demo -n supabase

helm list affiche la release, sa révision (1), son statut (deployed) et la version du chart.

Retrouver ce qui a été déployé :

helm get values demo -n supabase               # uniquement VOS values
helm get values demo -n supabase --all         # toutes les values calculées (chart + vous)
helm get manifest demo -n supabase | grep "^kind:" | sort | uniq -c
helm get notes demo -n supabase
Où Helm stocke-t-il tout cela ? Dans des Secrets du namespace, un par révision :

Ne les supprimez jamais : ce serait effacer la mémoire de Helm sur cette release.

Les objets Kubernetes créés :

kubectl get deployments,statefulsets,services,ingress
kubectl get pvc

Remarquez que la base de données tourne dans un StatefulSet, comme vu dans le TP précédent.

5.4 Les tests Helm

Un chart peut embarquer des tests : des Pods ou Jobs qui vérifient que l'application répond. Lancez-les une fois tous les Pods prêts :

helm test demo -n supabase --timeout 5m

Chaque test affiche Succeeded ou Failed. En cas d'échec, relancez après une ou deux minutes : un composant était peut-être encore en cours de démarrage.


Partie 6 : Utiliser Supabase

6.1 Accéder au tableau de bord

macOS et Windows : dans un terminal dédié, laissé ouvert :

minikube tunnel -p supabase

Récupérez les identifiants du tableau de bord depuis le Secret créé par le chart :

kubectl get secret demo-supabase-dashboard -o jsonpath='{.data.username}' | base64 -d; echo
kubectl get secret demo-supabase-dashboard -o jsonpath='{.data.password}' | base64 -d; echo

Ouvrez http://supabase.local et connectez-vous avec ces identifiants. Vous arrivez sur Supabase Studio.

Le trafic suit ce chemin :

Kong est l'unique porte d'entrée. Tous les autres Services sont en ClusterIP.

6.2 Créer une table

Dans Studio, ouvrez le SQL Editor et exécutez :

create table public.notes (
  id bigint generated always as identity primary key,
  texte text not null,
  cree_le timestamptz default now()
);

insert into public.notes (texte) values
  ('Première note déployée avec Helm'),
  ('Deuxième note');

-- Sécurité : activer les règles par ligne, puis autoriser la lecture publique
alter table public.notes enable row level security;
grant select on public.notes to anon;
create policy "lecture publique" on public.notes
  for select to anon using (true);

Vérifiez dans Table Editor que la table et ses deux lignes apparaissent.

Row Level Security (RLS) : une fois activée, aucune ligne n'est accessible via l'API tant qu'une règle (policy) ne l'autorise pas explicitement. C'est le mécanisme de sécurité central de Supabase.

6.3 Interroger l'API REST générée automatiquement

Supabase a créé une API REST pour la table, sans écrire une ligne de code. Récupérez la clé anon :

export ANON_KEY=$(kubectl get secret demo-supabase-jwt -o jsonpath='{.data.anonKey}' | base64 -d)
echo $ANON_KEY | cut -c1-30

Interrogez la table :

curl -s http://supabase.local/rest/v1/notes \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"; echo

Vous obtenez les deux notes au format JSON.

Tentez sans clé :

curl -s http://supabase.local/rest/v1/notes; echo

Kong refuse la requête : la clé est obligatoire.

6.4 Créer un utilisateur

curl -s -X POST http://supabase.local/auth/v1/signup \
  -H "apikey: $ANON_KEY" -H "Content-Type: application/json" \
  -d '{"email":"stagiaire@example.com","password":"MotDePasse-TP-2026"}' | head -c 300; echo

La réponse contient un access_token : l'utilisateur est créé et connecté (la confirmation par e-mail est désactivée par défaut dans le chart). Retrouvez-le dans Studio, menu Authentication.

✅ Point de contrôle 6

  1. Quel composant est la seule porte d'entrée HTTP de Supabase ?
  2. D'où vient la valeur de ANON_KEY récupérée dans le cluster ?
  3. Que se passerait-il pour la requête curl de la partie 6.3 sans la policy « lecture publique » ?

Partie 7 : Mettre à jour et revenir en arrière

7.1 Mettre à jour la configuration

On veut deux réplicas de l'API REST. Ajoutez à la fin de values-tp.yaml :

deployment:
  rest:
    replicaCount: 2

Vérifiez l'effet avant d'appliquer, en comparant le rendu actuel au rendu futur :

diff <(helm get manifest demo -n supabase) \
     <(helm template demo supabase/supabase --version 0.8.0 -n supabase --skip-tests -f values-tp.yaml -f values-secrets.yaml)

Seule la ligne replicas du Deployment demo-supabase-rest doit apparaître.

Le plugin helm-diff fait la même chose en plus lisible (helm diff upgrade ...). Il est très utilisé en production mais n'est pas nécessaire pour ce TP.

Appliquez :

helm upgrade demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml
helm list -n supabase
kubectl get deployment demo-supabase-rest

La release est passée en révision 2 et le Deployment compte 2 Pods.

7.2 L'historique

helm history demo -n supabase
helm get values demo -n supabase --revision 1 | grep -A2 "^deployment" || echo "(pas de section deployment en révision 1)"
helm get values demo -n supabase --revision 2 | grep -A2 "^deployment"

Chaque révision conserve ses values et ses manifestes.

7.3 Revenir en arrière

helm rollback demo 1 -n supabase
helm history demo -n supabase
kubectl get deployment demo-supabase-rest

Le Deployment est revenu à 1 réplica. Remarquez que le rollback crée une révision 3 (copie de la révision 1) : l'historique n'est jamais réécrit.

Ce que rollback ne fait pas : il restaure les manifestes, pas les données. Si une mise à jour avait modifié le schéma de la base, le rollback ne l'annulerait pas. Avant toute mise à jour importante (nouvelle version du chart, nouvelle version de PostgreSQL), on sauvegarde la base.

Remettez replicaCount: 1 ou supprimez le bloc deployment de values-tp.yaml pour que le fichier reflète l'état réel.

7.4 Piège n° 1 : oublier ses fichiers de values

Un helm upgrade repart des values par défaut du chart, puis applique uniquement ce que vous lui donnez sur la ligne de commande. Il ne reprend pas automatiquement les values de l'installation précédente.

Simulez un upgrade où l'on oublie les fichiers :

helm upgrade demo supabase/supabase --version 0.8.0 -n supabase \
  --set deployment.rest.replicaCount=2 --dry-run \
  | grep -A12 "name: demo-supabase-dashboard$" | grep username
echo c3VwYWJhc2U= | base64 -d; echo

L'utilisateur du tableau de bord redeviendrait supabase, et tous les secrets reprendraient les valeurs publiques par défaut. Les composants ne pourraient plus se connecter à la base (dont le mot de passe, lui, ne change pas) : panne générale.

Les bonnes pratiques :

Méthode Effet Recommandation
-f values-tp.yaml -f values-secrets.yaml on redonne toute la configuration recommandé : les fichiers sont la source de vérité
--reuse-values reprend les values de la révision précédente et ajoute les nouvelles pratique mais ignore les nouvelles valeurs par défaut d'un chart mis à jour
--reset-then-reuse-values repart des défauts du chart puis réapplique les anciennes values plus sûr que --reuse-values lors d'un changement de version du chart

Vérifiez avec --reuse-values :

helm upgrade demo supabase/supabase --version 0.8.0 -n supabase \
  --reuse-values --set deployment.rest.replicaCount=2 --dry-run \
  | grep -A12 "name: demo-supabase-dashboard$" | grep username

Cette fois, YWRtaW4= (admin) est conservé.

7.5 Piège n° 2 : les listes sont remplacées, pas fusionnées

Helm fusionne les dictionnaires (clés/valeurs) mais remplace entièrement les listes. Dans ce chart, environment.auth est une liste de variables d'environnement :

grep -n -A6 "^  auth:" values-defaut.yaml | sed -n '/environment/,$p' | head -n 0
awk '/^environment:/{f=1} f && /^  auth:/{g=1} g{print; n++} n>8{exit}' values-defaut.yaml

Si vous écrivez dans vos values :

environment:
  auth:
    - name: GOTRUE_DISABLE_SIGNUP
      value: "true"

la liste complète du chart (une trentaine de variables : URL du site, JWT, configuration e-mail...) est remplacée par cette seule variable. Le service d'authentification ne démarrerait plus correctement.

Vérifiez :

helm template demo supabase/supabase --version 0.8.0 --skip-tests \
  --show-only templates/auth/deployment.yaml | grep -c "name: GOTRUE_"

cat > test-liste.yaml <<'EOF'
environment:
  auth:
    - name: GOTRUE_DISABLE_SIGNUP
      value: "true"
EOF

helm template demo supabase/supabase --version 0.8.0 --skip-tests -f test-liste.yaml \
  --show-only templates/auth/deployment.yaml | grep -c "name: GOTRUE_"

Le nombre de variables GOTRUE_ s'effondre. Pour modifier une liste, il faut recopier la liste complète depuis values-defaut.yaml, puis y faire sa modification.

✅ Point de contrôle 7

  1. Que contient une révision Helm ?
  2. Pourquoi helm rollback crée-t-il une nouvelle révision au lieu de revenir à la révision 1 ?
  3. Que se passe-t-il si on lance helm upgrade avec seulement --set ?
  4. Comment modifier une seule variable d'une liste de values sans casser les autres ?

Partie 8 : Désinstaller sans perdre ses données

8.1 Désinstaller

helm uninstall demo -n supabase
helm list -n supabase
kubectl get pods
kubectl get pvc

Les Pods, Services, Secrets et la plupart des PVC ont disparu. Il reste demo-supabase-db et demo-supabase-pgsodium : ce sont les deux PVC annotés helm.sh/resource-policy: keep dans values-tp.yaml.

Sans cette annotation, helm uninstall aurait supprimé le PVC de la base, et la StorageClass de minikube (reclaimPolicy: Delete) aurait détruit les données. Beaucoup de charts créent leurs PVC ainsi : vérifiez toujours ce point avant une désinstallation.

8.2 Réinstaller et retrouver ses données

Réinstallez avec le même nom de release et les mêmes fichiers :

helm install demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml
kubectl get pods -l app.kubernetes.io/instance=demo -w     # Ctrl+C quand tout est Running

Helm adopte les PVC existants : ils portent encore les annotations indiquant qu'ils appartiennent à la release demo du namespace supabase.

Vérifiez :

export ANON_KEY=$(kubectl get secret demo-supabase-jwt -o jsonpath='{.data.anonKey}' | base64 -d)
curl -s http://supabase.local/rest/v1/notes \
  -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"; echo

Les notes et l'utilisateur créé en partie 6 sont toujours là.

Trois conditions ont rendu cela possible :l'annotation keep sur les PVC ;le même nom de release (les PVC sont nommés <release>-supabase-db) ;les mêmes secrets : la base a été initialisée avec le mot de passe de values-secrets.yaml. D'où l'importance de conserver ce fichier en lieu sûr.

L'historique Helm, lui, repart à la révision 1 : il a été supprimé avec la release.

Une alternative à l'annotation keep : créer le PVC soi-même et le passer au chart avec persistence.db.existingClaim. Le chart ne le gère alors pas du tout.

✅ Point de contrôle 8

  1. Pourquoi certains PVC ont-ils survécu à helm uninstall ?
  2. Que se passerait-il si l'on réinstallait avec un nouveau values-secrets.yaml généré par le script ?

Partie 9 : Exercices

Exercice 1 : exposer l'API en NodePort

En plus de l'Ingress, on veut accéder à Kong par un Service NodePort.

  1. Trouvez dans values-defaut.yaml la valeur à modifier.
  2. Ajoutez-la dans values-tp.yaml et vérifiez le rendu avec helm template --show-only.
  3. Appliquez avec helm upgrade.
  4. Récupérez l'URL avec minikube service demo-supabase-kong -n supabase -p supabase --url et interrogez /rest/v1/notes sur cette URL.
  5. Question : on voudrait fixer le port à 30800. Cherchez dans les values une clé nodePort pour Kong. Existe-t-elle ? Lisez supabase/templates/kong/service.yaml et expliquez pourquoi c'est impossible sans modifier le chart. Quelles solutions existent ?

Exercice 2 : sortir un secret des values

Garder des mots de passe dans un fichier de values reste risqué. Le chart permet de référencer un Secret Kubernetes créé à part avec la clé secretRef.

  1. Dans values-defaut.yaml, repérez secretRef dans la section secret.dashboard.
  2. Créez vous-même un Secret supabase-dashboard contenant username, password et openAiApiKey avec kubectl create secret generic.
  3. Modifiez vos values pour que le chart utilise ce Secret.
  4. Avec helm template, vérifiez que le chart ne génère plus le Secret demo-supabase-dashboard.
  5. Appliquez, puis connectez-vous au tableau de bord avec les nouveaux identifiants.

Exercice 3 : questions de synthèse

  1. Un collègue lance helm install demo supabase/supabase sans --version ni -n. Citez deux problèmes possibles.
  2. Où pouvez-vous voir les values utilisées lors de la révision 2 d'une release ?
  3. Quelle commande permet de voir les manifestes qu'un helm upgrade produirait, sans rien appliquer ?

Partie 10 : Nettoyage

Supprimer la release et les données conservées :

helm uninstall demo -n supabase
kubectl delete pvc --all -n supabase
kubectl delete namespace supabase
kubectl config set-context --current --namespace=default

Supprimer le cluster et l'entrée du fichier hosts :

minikube delete -p supabase
sudo sed -i.bak '/supabase\.local/d' /etc/hosts      # Linux / macOS

Sous Windows, retirez la ligne à la main dans le fichier hosts.

Pour revenir au cluster des TP précédents : kubectl config use-context minikube.


Annexe A : Aide-mémoire Helm

Dépôts et charts

Action Commande
Ajouter un dépôt helm repo add <nom> <url>
Mettre à jour les index helm repo update
Chercher un chart helm search repo <mot>
Lister les versions helm search repo <dépôt/chart> --versions
Voir les values par défaut helm show values <dépôt/chart> --version X
Voir la documentation helm show readme <dépôt/chart>
Télécharger et décompresser helm pull <dépôt/chart> --version X --untar

Rendu et validation

Action Commande
Générer les manifestes localement helm template <release> <chart> -f values.yaml
Un seul fichier du rendu ... --show-only templates/<chemin>.yaml
Exclure les tests ... --skip-tests
Vérifier un chart helm lint <dossier-chart> -f values.yaml
Simuler une installation helm install ... --dry-run

Cycle de vie

Action Commande
Installer helm install <release> <chart> --version X -n <ns> -f values.yaml
Installer ou mettre à jour helm upgrade --install <release> <chart> ...
Mettre à jour helm upgrade <release> <chart> --version X -n <ns> -f values.yaml
Attendre que tout soit prêt ... --wait --timeout 15m
Lister les releases helm list -n <ns> (ou -A)
État d'une release helm status <release> -n <ns>
Historique helm history <release> -n <ns>
Revenir en arrière helm rollback <release> <révision> -n <ns>
Tester helm test <release> -n <ns>
Désinstaller helm uninstall <release> -n <ns>

Inspection d'une release

Action Commande
Values fournies par l'utilisateur helm get values <release>
Toutes les values calculées helm get values <release> --all
Values d'une révision helm get values <release> --revision N
Manifestes appliqués helm get manifest <release>
Notes helm get notes <release>

Annexe B : Dépannage

Symptôme Cause probable Vérification / solution
Error: INSTALLATION FAILED: cannot re-use a name that is still in use Une release du même nom existe déjà helm list -n supabase, utiliser helm upgrade
Error: ... chart "supabase" matching 0.8.0 not found Index du dépôt pas à jour helm repo update
Pods en Pending Mémoire ou CPU insuffisants kubectl describe pod, recréer le cluster avec plus de ressources
Pods en ImagePullBackOff Réseau lent ou limite de téléchargement Docker Hub kubectl describe pod, patienter puis kubectl delete pod pour relancer
auth, rest ou storage en CrashLoopBackOff au démarrage La base n'est pas encore prête Patienter quelques minutes, vérifier kubectl logs statefulset/demo-supabase-db
Erreurs d'authentification à la base après modification des secrets Mot de passe fixé à la première initialisation Remettre l'ancien values-secrets.yaml, ou tout supprimer (PVC compris) et réinstaller
supabase.local ne répond pas (Linux) Ligne hosts absente ou IP incorrecte grep supabase /etc/hosts, comparer avec minikube ip -p supabase
supabase.local ne répond pas (macOS / Windows) minikube tunnel non lancé Lancer minikube tunnel -p supabase dans un terminal dédié
404 sur supabase.local Contrôleur ingress absent minikube addons enable ingress -p supabase
{"message":"No API key found in request"} En-tête apikey absent Vérifier echo $ANON_KEY
La requête REST renvoie [] RLS activée sans policy de lecture Vérifier la partie 6.2
Réinstallation : invalid ownership metadata Le PVC conservé appartient à une autre release Réinstaller avec le même nom de release et le même namespace
helm test échoue Composant encore en démarrage Relancer après 1 ou 2 minutes, kubectl logs du Pod de test

Annexe C : Corrigés

Point de contrôle 2

  1. analytics, vector et minio (enabled: false dans deployment).
  2. supabase/postgres en version 17.6.1.136 (section image.db).
  3. 5Gi (persistence.db.size).
  4. this_password_is_insecure_and_should_be_updated. Ce mot de passe est public : toute installation qui le conserve est accessible à n'importe qui. Il en va de même pour les clés JWT par défaut.
  5. supabase.local.

Point de contrôle 3

  1. helm template produit les manifestes localement sans contacter le cluster ni créer de release. helm install les envoie au cluster et enregistre une release.
  2. À la valeur de la clé size dans persistence → db, après fusion des values par défaut et des vôtres.
  3. demo est le nom de la release. Les helpers du chart l'utilisent comme préfixe, ce qui permet d'installer plusieurs fois le même chart dans un namespace sous des noms différents.

Point de contrôle 4

  1. Pour pouvoir mettre à jour le chart sans perdre ses modifications, et pour garder un fichier court qui ne contient que ce qui diffère du standard.
  2. 3Gi : --set a la priorité la plus forte.
  3. Elle contourne la Row Level Security. Toute personne qui la récupère (elle serait visible dans le code du navigateur) aurait un accès total à la base.

Point de contrôle 6

  1. Kong, la passerelle API, exposée par le Service demo-supabase-kong et l'Ingress.
  2. Du Secret demo-supabase-jwt, créé par le chart à partir de secret.jwt.anonKey dans values-secrets.yaml.
  3. Elle renverrait une liste vide [] : la RLS bloque toutes les lignes tant qu'aucune règle ne les autorise.

Point de contrôle 7

  1. Les values utilisées, les manifestes générés, la version du chart et le statut.
  2. L'historique est conservé en ajout seul : on garde la trace qu'un retour arrière a eu lieu, et on peut revenir à nouveau sur la révision 2 si besoin.
  3. Helm repart des values par défaut du chart et n'applique que le --set : toutes les autres personnalisations, y compris les secrets, sont perdues.
  4. Recopier la liste complète depuis les values par défaut dans son fichier, puis modifier ou ajouter la variable voulue.

Point de contrôle 8

  1. Ils portent l'annotation helm.sh/resource-policy: keep, que Helm respecte lors d'une désinstallation.
  2. Les composants utiliseraient un nouveau mot de passe alors que la base a été initialisée avec l'ancien : ils ne pourraient plus s'y connecter. De plus, les clés JWT changeraient et les jetons existants deviendraient invalides.

Exercice 1

Ajout dans values-tp.yaml :

service:
  kong:
    type: NodePort
helm template demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml --show-only templates/kong/service.yaml

helm upgrade demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml

kubectl get svc demo-supabase-kong
URL=$(minikube service demo-supabase-kong -n supabase -p supabase --url)    # Linux
curl -s $URL/rest/v1/notes -H "apikey: $ANON_KEY" -H "Authorization: Bearer $ANON_KEY"; echo

Sous macOS et Windows, minikube service ... --url ouvre un tunnel et doit rester actif : copiez l'URL affichée dans un second terminal.

Question 5 : il n'y a pas de clé nodePort pour Kong. Le template kong/service.yaml n'écrit jamais de champ nodePort : un chart n'expose que ce que ses templates paramètrent. Kubernetes attribue donc un port aléatoire. Solutions :

  • proposer une modification au chart (contribution au projet) ;
  • utiliser un post-renderer Helm, par exemple Kustomize, pour patcher les manifestes générés avant leur envoi ;
  • désactiver l'exposition du chart et créer son propre Service NodePort à côté, avec le même selector ;
  • maintenir une copie modifiée du chart (à éviter : chaque mise à jour devient un travail de fusion).

Exercice 2

kubectl create secret generic supabase-dashboard \
  --from-literal=username=formateur \
  --from-literal=password="$(openssl rand -hex 12)" \
  --from-literal=openAiApiKey=non-utilise

Dans values-secrets.yaml, remplacez le bloc dashboard par :

  dashboard:
    secretRef: supabase-dashboard
helm template demo supabase/supabase --version 0.8.0 -n supabase --skip-tests \
  -f values-tp.yaml -f values-secrets.yaml | grep -c "name: demo-supabase-dashboard$"
# résultat attendu : 0

helm upgrade demo supabase/supabase --version 0.8.0 -n supabase \
  -f values-tp.yaml -f values-secrets.yaml
kubectl rollout status deployment demo-supabase-kong
kubectl get secret supabase-dashboard -o jsonpath='{.data.password}' | base64 -d; echo

Le Secret n'est plus géré par Helm : un helm uninstall ne le supprimera pas, et il peut être produit par un outil dédié (Sealed Secrets, External Secrets, Vault...).

Exercice 3

  1. Sans --version, la version installée dépend de la date : le déploiement n'est pas reproductible. Sans -n, la release part dans le namespace du contexte courant, peut-être default, et les commandes suivantes avec -n supabase ne la trouveront pas.
  2. helm get values demo -n supabase --revision 2.
  3. helm upgrade ... --dry-run, ou helm template avec les mêmes values comparé à helm get manifest (partie 7.1), ou le plugin helm-diff.

Pour aller plus loin

  • Écrire son propre chart : helm create mon-app génère un chart d'exemple complet. Reprenez les manifestes WordPress du TP précédent et transformez-les en chart.
  • Dépendances : un chart peut en inclure d'autres (section dependencies de Chart.yaml), par exemple un chart PostgreSQL.
  • Registres OCI : les charts peuvent être publiés dans un registre de conteneurs (helm push, helm install oci://...).
  • helmfile ou Argo CD : décrire plusieurs releases dans des fichiers versionnés et les synchroniser automatiquement avec le cluster (GitOps).
  • Production : base de données répliquée (opérateur PostgreSQL), TLS sur l'Ingress avec cert-manager, secrets gérés par un outil dédié, sauvegardes planifiées.