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 :
- démarrer un cluster Kubernetes local avec minikube ;
- créer, inspecter et supprimer un Pod ;
- déployer une application avec un Deployment (réplicas, auto-réparation, mise à l'échelle, mise à jour, retour arrière) ;
- exposer cette application avec un Service (ClusterIP puis NodePort) ;
- comprendre le rôle des labels et des selectors, qui relient tous ces objets entre eux.
Sommaire
- Partie 0 : Installation et démarrage
- Partie 1 : Premiers pas avec kubectl
- Partie 2 : Le Pod
- Partie 3 : Le Deployment
- Partie 4 : Le Service
- Partie 5 : Exercice de synthèse
- Partie 6 : Nettoyage
- Annexe A : Aide-mémoire kubectl
- Annexe B : Dépannage
- Annexe C : Corrigés
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 commandedockerdemandesudo, 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 danskube-system. Vos ressources iront par défaut dans le namespacedefault.
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é :
- Quelle commande affiche l'IP d'un Pod ?
- Où trouver la raison d'un Pod bloqué en
PendingouImagePullBackOff? - 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 :templateest le modèle à partir duquel chaque Pod sera fabriqué. Son contenu est identique aumetadata+specdupod.yamlprécédent.selector.matchLabelsdoit correspondre aux labels dutemplate. Sinon Kubernetes refuse le Deployment.resources.requestsindique ce que le Pod réserve sur le nœud (utilisé par le scheduler pour choisir où le placer).limitsfixe un plafond : au-delà de la mémoire limite, le conteneur est tué (OOMKilled).50msignifie 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 prochainkubectl applydu 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
Remettezdeployment.yamlen cohérence : vérifiez qu'il contient bienv1.10.3. Sinon, le prochainapplyproduirait un résultat inattendu.
✅ Point de contrôle 3
- Quel objet crée réellement les Pods : le Deployment ou le ReplicaSet ?
- Pourquoi le
selectordoit-il correspondre aux labels dutemplate? - 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 nomwebsuffit : le DNS interne du cluster résout le nom du Service. Le nom complet est<service>.<namespace>.svc.cluster.local.La valeurHostnamechange d'une requête à l'autre : le Service répartit les requêtes entre les Pods.--rmsupprime le Podclientà 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
- Pourquoi ne faut-il pas utiliser l'IP d'un Pod directement ?
- Comment un Service sait-il vers quels Pods envoyer le trafic ?
- Quelle différence entre
port,targetPortetnodePort?
Partie 5 : Exercice de synthèse
Sans copier les fichiers précédents (utilisez kubectl explain si besoin), déployez une seconde application :
Consignes :
- Un fichier unique
api.yamlcontenant à la fois un Deployment et un Service (séparez les deux documents YAML par une ligne---). - Deployment nommé
api, imagenginx:1.27, 2 réplicas, labelapp: api,tier: backend. - Service nommé
apide type NodePort sur le port31000, qui cible uniquement les Podsapp: api. - Vérifiez que
curlsur l'URL NodePort renvoie la page d'accueil nginx. - Mettez l'image à jour en
nginx:1.28en modifiant le fichier, puis vérifiez le déroulement aveckubectl rollout status. - Depuis un Pod
busyboxtemporaire, vérifiez que le Serviceapiest 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
kubectl get pods -o wide(oukubectl describe pod <nom>).- Dans la section Events de
kubectl describe pod <nom>. - Il disparaît définitivement : aucun contrôleur ne le surveille ni ne le recrée.
Point de contrôle 3
- Le ReplicaSet. Le Deployment gère les ReplicaSets (un par version du template) et orchestre le passage de l'un à l'autre.
- 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.
- La stratégie
RollingUpdatene 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
- Elle change à chaque recréation du Pod, et elle ne désigne qu'un seul Pod parmi plusieurs réplicas.
- Par son
selector: il envoie le trafic à tous les Pods prêts dont les labels correspondent. 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 :
- comprendre pourquoi un conteneur perd ses données et comment les volumes règlent ce problème ;
- demander du stockage persistant avec un PersistentVolumeClaim (PVC) ;
- stocker des mots de passe dans un Secret ;
- déployer une application en deux tiers : WordPress + base de données MariaDB ;
- faire communiquer deux applications par un Service ClusterIP ;
- exposer WordPress (et phpMyAdmin en exercice) avec des Services NodePort ;
- vérifier que les données survivent à la suppression des Pods ;
- migrer la base de données vers un StatefulSet et comprendre ce qui le distingue d'un Deployment.
Sommaire
- Partie 0 : Préparation
- Partie 1 : Architecture cible
- Partie 2 : Comprendre les volumes
- Partie 3 : Le Secret des mots de passe
- Partie 4 : La base de données MariaDB
- Partie 5 : WordPress
- Partie 6 : Installer WordPress et tester la persistance
- Partie 7 : Exercice, ajouter phpMyAdmin en NodePort
- Partie 8 : Migrer MariaDB vers un StatefulSet
- Partie 9 : Nettoyage
- Annexe A : Dépannage
- Annexe B : Corrigés
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:30090Avec 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 namespacewordpress. 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êmename.
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
- Pourquoi un fichier écrit dans un conteneur disparaît-il quand le Pod est recréé ?
- Quelle est la différence entre un PVC et un PV ?
- 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.secretKeyRefinjecte une valeur du Secret dans une variable d'environnement. Le mot de passe n'apparaît jamais dansmariadb.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. AvecRecreate, 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 variablesMARIADB_*ne sont lues qu'au tout premier démarrage, quand le dossier/var/lib/mysqlest 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
- Pourquoi MariaDB utilise-t-elle un Service ClusterIP plutôt que NodePort ?
- Pourquoi la stratégie
Recreateplutôt queRollingUpdate?
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/htmlest monté sur le volume. Au premier démarrage, l'image copie les fichiers WordPress dans ce dossier vide et génèrewp-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--portsdu 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 :
- choisissez la langue ;
- renseignez le titre du site, un identifiant administrateur et un mot de passe ;
- connectez-vous au tableau de bord.
Puis, pour avoir des données à protéger :
- créez un article intitulé « Test de persistance Kubernetes » et publiez-le ;
- 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
- Où sont stockés les articles ? Où sont stockées les images téléversées ?
- Après
kubectl delete deployment wordpress mariadb, pourquoi les données sont-elles toujours là ? - 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 :
- Créez
phpmyadmin.yamlcontenant un Deployment et un Service. - Image :
phpmyadmin:5.2, labelapp: phpmyadmin, 1 réplica, port conteneur 80. - phpMyAdmin doit se connecter à la base via la variable d'environnement
PMA_HOST. Quelle valeur lui donner ? - phpMyAdmin n'a pas besoin de volume : il ne stocke aucune donnée. Pourquoi ?
- Service de type NodePort exposé sur le port 30091.
- Ouvrez http://localhost:30091, connectez-vous avec l'utilisateur
wpuseret retrouvez votre article dans la tablewp_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 :serviceNamerelie le StatefulSet à son Service headless. Chaque Pod devient joignable par<pod>.<service-headless>, par exemplemariadb-0.mariadb-headless.Il n'y a plus de blocvolumesni de PVC déclaré à part.volumeClaimTemplatesest un modèle : pour chaque Pod, Kubernetes crée un PVC nommé<nom du template>-<nom du Pod>, doncdata-mariadb-0,data-mariadb-1...Pas destrategy: Recreate: un StatefulSet ne lance jamais deux Pods portant le même nom.mariadb-0est toujours arrêté avant que son remplaçant ne démarre.Les Pods gardent le labelapp: mariadb. Le Servicemariadbutilisé 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-0a été créé automatiquement, à côté de l'ancienmariadb-data; - le Service
mariadb-headlessafficheNonedans la colonneCLUSTER-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-1n'est créé qu'une foismariadb-0prêt, puismariadb-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-1etmariadb-2sont 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 Servicemariadbré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 champpersistentVolumeClaimRetentionPolicydu StatefulSet (whenScaledetwhenDeleted, valeurRetainpar 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 :
- Repérez le label qui contient le nom du Pod.
- Modifiez le Service
mariadbdansmariadb-statefulset.yamlpour qu'il ne cible quemariadb-0. - Appliquez, puis passez le StatefulSet à 3 réplicas.
- Relancez la boucle
curl: toutes les réponses doivent maintenant être correctes (200ou redirection vers la page d'accueil, jamais versinstall.php). - Revenez à 1 réplica et supprimez les PVC en trop.
✅ Point de contrôle 8
- Citez trois différences entre un Deployment et un StatefulSet.
- À quoi sert un Service headless ?
- Quel sera le nom du PVC du Pod
mariadb-2? - Après
kubectl scale statefulset mariadb --replicas=3, pourquoi WordPress propose-t-il parfois une nouvelle installation ? - 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
- 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.
- 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.
- Le PV associé est supprimé et les données avec lui.
Point de contrôle 4
- Seul WordPress, à l'intérieur du cluster, a besoin de joindre la base. L'exposer à l'extérieur augmenterait inutilement la surface d'attaque.
RollingUpdateferait tourner brièvement deux instances MariaDB sur les mêmes fichiers, avec un risque de corruption.Recreatearrête l'ancien Pod avant de démarrer le nouveau. C'est aussi nécessaire avec un volumeReadWriteOncesur un cluster multi-nœuds.
Point de contrôle 6
- Les articles sont dans la base MariaDB (table
wp_posts), donc dans le PVCmariadb-data. Les images sont dans/var/www/html/wp-content/uploads, donc dans le PVCwordpress-data. - 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.
- Un volume
ReadWriteOncene 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
- 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. - 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. data-mariadb-2: nom duvolumeClaimTemplatesuivi du nom du Pod.mariadb-1etmariadb-2sont des bases indépendantes et vides. Le StatefulSet ne réplique pas les données, et le Servicemariadbrépartit les connexions entre les trois Pods.- 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 :
- expliquer ce que sont un chart, une release, un dépôt et les values Helm ;
- explorer un chart avant de l'installer (
helm show,helm template) ; - comprendre comment un template transforme des values en manifestes Kubernetes ;
- écrire vos propres fichiers de values et générer des secrets propres ;
- installer, inspecter, mettre à jour et revenir en arrière sur une release ;
- éviter les deux pièges classiques de
helm upgrade; - 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
- Partie 1 : Les notions de Helm
- Partie 2 : Explorer un chart sans l'installer
- Partie 3 : Comprendre le templating
- Partie 4 : Préparer sa configuration
- Partie 5 : Installer et inspecter la release
- Partie 6 : Utiliser Supabase
- Partie 7 : Mettre à jour et revenir en arrière
- Partie 8 : Désinstaller sans perdre ses données
- Partie 9 : Exercices
- Partie 10 : Nettoyage
- Annexe A : Aide-mémoire Helm
- Annexe B : Dépannage
- Annexe C : Corrigés
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 :
- Quels composants sont désactivés par défaut ? (cherchez
enabled: falsedans la sectiondeployment) - Quelle image et quel tag sont utilisés pour la base de données ?
- Quelle taille de disque est demandée par défaut pour la base de données ?
- 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 ?
- 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.typeva chercher la valeurservice→kong→typedans les values. C'est le lien direct entrevalues.yamlet 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.tplqui 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 objetsJobde 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
- Quelle différence entre
helm templateethelm install? - Dans un template, à quoi correspond
{{ .Values.persistence.db.size }}? - 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'annotationhelm.sh/resource-policy: keepdemande à Helm de ne pas supprimer cette ressource lors d'unhelm 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
- Pourquoi ne modifie-t-on pas directement le
values.yamldu chart ? - Si
values-tp.yamlindiquesize: 2Giet qu'on ajoute--set persistence.db.size=3Gi, quelle taille sera utilisée ? - Pourquoi la clé
service_rolene 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 -f5.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
- Quel composant est la seule porte d'entrée HTTP de Supabase ?
- D'où vient la valeur de
ANON_KEYrécupérée dans le cluster ? - Que se passerait-il pour la requête
curlde la partie 6.3 sans lapolicy« 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
- Que contient une révision Helm ?
- Pourquoi
helm rollbackcrée-t-il une nouvelle révision au lieu de revenir à la révision 1 ? - Que se passe-t-il si on lance
helm upgradeavec seulement--set? - 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 uninstallaurait 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'annotationkeepsur 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 devalues-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
- Pourquoi certains PVC ont-ils survécu à
helm uninstall? - Que se passerait-il si l'on réinstallait avec un nouveau
values-secrets.yamlgé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.
- Trouvez dans
values-defaut.yamlla valeur à modifier. - Ajoutez-la dans
values-tp.yamlet vérifiez le rendu avechelm template --show-only. - Appliquez avec
helm upgrade. - Récupérez l'URL avec
minikube service demo-supabase-kong -n supabase -p supabase --urlet interrogez/rest/v1/notessur cette URL. - Question : on voudrait fixer le port à
30800. Cherchez dans les values une clénodePortpour Kong. Existe-t-elle ? Lisezsupabase/templates/kong/service.yamlet 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.
- Dans
values-defaut.yaml, repérezsecretRefdans la sectionsecret.dashboard. - Créez vous-même un Secret
supabase-dashboardcontenantusername,passwordetopenAiApiKeyaveckubectl create secret generic. - Modifiez vos values pour que le chart utilise ce Secret.
- Avec
helm template, vérifiez que le chart ne génère plus le Secretdemo-supabase-dashboard. - Appliquez, puis connectez-vous au tableau de bord avec les nouveaux identifiants.
Exercice 3 : questions de synthèse
- Un collègue lance
helm install demo supabase/supabasesans--versionni-n. Citez deux problèmes possibles. - Où pouvez-vous voir les values utilisées lors de la révision 2 d'une release ?
- Quelle commande permet de voir les manifestes qu'un
helm upgradeproduirait, 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
analytics,vectoretminio(enabled: falsedansdeployment).supabase/postgresen version17.6.1.136(sectionimage.db).5Gi(persistence.db.size).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.supabase.local.
Point de contrôle 3
helm templateproduit les manifestes localement sans contacter le cluster ni créer de release.helm installles envoie au cluster et enregistre une release.- À la valeur de la clé
sizedanspersistence→db, après fusion des values par défaut et des vôtres. demoest 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
- 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.
3Gi:--seta la priorité la plus forte.- 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
- Kong, la passerelle API, exposée par le Service
demo-supabase-konget l'Ingress. - Du Secret
demo-supabase-jwt, créé par le chart à partir desecret.jwt.anonKeydansvalues-secrets.yaml. - Elle renverrait une liste vide
[]: la RLS bloque toutes les lignes tant qu'aucune règle ne les autorise.
Point de contrôle 7
- Les values utilisées, les manifestes générés, la version du chart et le statut.
- 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.
- 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. - 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
- Ils portent l'annotation
helm.sh/resource-policy: keep, que Helm respecte lors d'une désinstallation. - 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
- 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-êtredefault, et les commandes suivantes avec-n supabasene la trouveront pas. helm get values demo -n supabase --revision 2.helm upgrade ... --dry-run, ouhelm templateavec 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-appgé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
dependenciesdeChart.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.