45 minutes (+ exercice optionnel)
Ce manifeste reconstruit l'application complète telle que laissée à la fin du TP 5 : les 5 briques, le StatefulSet de base de données et les deux interfaces exposées. C'est le plus gros Point de départ de la formation : appliquez-le dès la première minute, les images se téléchargeront pendant l'exercice 1.
Créez le fichier fil-rouge-tp06.yaml :
# fil-rouge-tp06.yaml
apiVersion: v1
kind: Namespace
metadata:
name: voting-app
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote
namespace: voting-app
labels:
app: vote
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
app: vote
template:
metadata:
labels:
app: vote
tier: frontend
spec:
containers:
- name: vote
image: dockersamples/examplevotingapp_vote:latest
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: voting-app
labels:
app: redis
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
tier: backend
spec:
containers:
- name: redis
image: redis:8-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: worker
namespace: voting-app
labels:
app: worker
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: worker
template:
metadata:
labels:
app: worker
tier: backend
spec:
containers:
- name: worker
image: dockersamples/examplevotingapp_worker:latest
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "512Mi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: result
namespace: voting-app
labels:
app: result
tier: frontend
spec:
replicas: 1
selector:
matchLabels:
app: result
template:
metadata:
labels:
app: result
tier: frontend
spec:
containers:
- name: result
image: dockersamples/examplevotingapp_result:latest
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: voting-app
labels:
app: db
spec:
clusterIP: None
selector:
app: db
ports:
- port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: voting-app
labels:
app: db
tier: database
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
tier: database
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
value: "postgres"
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: voting-app
labels:
app: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: vote
namespace: voting-app
labels:
app: vote
spec:
type: NodePort
selector:
app: vote
ports:
- port: 80
targetPort: 80
nodePort: 31000
---
apiVersion: v1
kind: Service
metadata:
name: result
namespace: voting-app
labels:
app: result
spec:
type: NodePort
selector:
app: result
ports:
- port: 80
targetPort: 80
nodePort: 31001
Puis appliquez-le et placez-vous dans le namespace :
kubectl apply -f fil-rouge-tp06.yaml
kubectl config set-context --current --namespace=voting-app
kubectl wait --for=condition=Available deployment --all --timeout=240s
kubectl get pods
NAME READY STATUS RESTARTS AGE
db-0 1/1 Running 0 2m
redis-6f4b8d7c9d-x2kqp 1/1 Running 0 2m
result-7c9f8b6d5e-k4wpz 1/1 Running 0 2m
vote-7d94f6c8b5-4x8mn 1/1 Running 0 2m
vote-7d94f6c8b5-jw8tn 1/1 Running 0 2m
vote-7d94f6c8b5-qp2rk 1/1 Running 0 2m
worker-5b8f9d6c47-t8slk 1/1 Running 1 (90s ago) 2m
Découvrir les ConfigMaps et les deux méthodes pour injecter des configurations dans les Pods : variables d'environnement et fichiers.
Créez d'abord le fichier app-config.yaml :
# app-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
app: demo-app
data:
# Variables simples
DATABASE_HOST: "postgres.example.com"
DATABASE_PORT: "5432"
LOG_LEVEL: "info"
MAX_CONNECTIONS: "100"
# Fichier de configuration complet
nginx.conf: |
events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name localhost;
location / {
return 200 'Hello from ConfigMap!';
add_header Content-Type text/plain;
}
}
}
Puis créez la ConfigMap :
kubectl apply -f app-config.yaml
kubectl describe configmap app-config
Créez d'abord le fichier app-with-env-vars.yaml :
# app-with-env-vars.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-env-vars
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"]
args:
- |
echo "=== Variables d'environnement depuis ConfigMap ==="
env | grep -E "(DATABASE|LOG|MAX)"
sleep 3600
# Toutes les clés de la ConfigMap deviennent des variables
envFrom:
- configMapRef:
name: app-config
Puis créez le Pod et vérifiez :
kubectl apply -f app-with-env-vars.yaml
kubectl logs app-with-env-vars
=== Variables d'environnement depuis ConfigMap ===
DATABASE_HOST=postgres.example.com
DATABASE_PORT=5432
LOG_LEVEL=info
MAX_CONNECTIONS=100
💡 Pour importer une seule clé plutôt que toutes, on utilise env + valueFrom.configMapKeyRef (nom de ConfigMap + clé).
Créez d'abord le fichier app-with-volumes.yaml :
# app-with-volumes.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-volumes
spec:
containers:
- name: nginx
image: nginx:1.29.5
ports:
- containerPort: 80
volumeMounts:
- name: config-volume
mountPath: /etc/nginx/
readOnly: true
volumes:
- name: config-volume
configMap:
name: app-config
items:
- key: nginx.conf
path: nginx.conf
Puis créez le Pod et testez :
kubectl apply -f app-with-volumes.yaml
# La clé nginx.conf est devenue un fichier
kubectl exec app-with-volumes -- cat /etc/nginx/nginx.conf
# Et nginx sert la réponse définie dans la ConfigMap
kubectl exec app-with-volumes -- curl -s localhost
Hello from ConfigMap!
configMapKeyRef et envFrom: configMapRef ?Q: Quelle est la différence entre configMapKeyRef et envFrom: configMapRef ?
R:
configMapKeyRef : Importe une clé spécifique de la ConfigMap comme variable d'environnementenvFrom: configMapRef : Importe toutes les clés de la ConfigMap comme variables d'environnementQ: Pourquoi utiliser des volumes plutôt que des variables d'environnement pour les fichiers de configuration ?
R: Les volumes conviennent aux fichiers complets et aux formats complexes (nginx.conf, JSON, YAML), et leur contenu est rafraîchi automatiquement en cas de modification de la ConfigMap (avec un délai), contrairement aux variables d'environnement, figées au démarrage du processus.
Externaliser la configuration de l'interface de vote : les intitulés OPTION_A/OPTION_B sortent du manifeste applicatif pour rejoindre une ConfigMap.
Créez d'abord le fichier voting-options-configmap.yaml :
# voting-options-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: voting-options
namespace: voting-app
labels:
app: vote
data:
OPTION_A: "Chats"
OPTION_B: "Chiens"
kubectl apply -f voting-options-configmap.yaml
Créez le fichier vote-deployment-configmap.yaml (le Deployment vote, désormais consommateur de la ConfigMap) :
# vote-deployment-configmap.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote
namespace: voting-app
labels:
app: vote
tier: frontend
spec:
replicas: 3
selector:
matchLabels:
app: vote
template:
metadata:
labels:
app: vote
tier: frontend
spec:
containers:
- name: vote
image: dockersamples/examplevotingapp_vote:latest
envFrom:
- configMapRef:
name: voting-options
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
kubectl apply -f vote-deployment-configmap.yaml
kubectl rollout status deployment/vote
Ouvrez le port 31000 (menu ☰ → Traffic / Ports) : les boutons affichent Chats et Chiens. La configuration est désormais externalisée : même image, comportement piloté par la ConfigMap.
kubectl edit configmap voting-options
# Dans l'éditeur, remplacez :
# OPTION_B: "Chiens"
# par :
# OPTION_B: "Lapins"
Rechargez la page de vote : toujours Chiens. Les variables d'environnement sont injectées au démarrage du processus : les Pods en cours d'exécution ne voient pas la modification.
kubectl rollout restart deployment/vote
kubectl rollout status deployment/vote
Rechargez la page : Chats vs Lapins. ✅
💡 rollout restart recrée les Pods par vagues (comme un rolling update) : chaque nouveau Pod relit la ConfigMap au démarrage. C'est le geste standard après une modification de configuration consommée en variables d'environnement.
Q: Pourquoi la modification de la ConfigMap n'a-t-elle pas suffi ?
R: Les variables d'environnement sont figées au démarrage du processus. Modifier la ConfigMap ne change pas le template des Pods : aucun rollout n'est déclenché automatiquement, il faut kubectl rollout restart pour que de nouveaux Pods relisent les valeurs.
Q: Quel est l'intérêt d'avoir externalisé ces valeurs plutôt que de les écrire dans le Deployment ?
R: La configuration est séparée du déploiement applicatif : on peut la modifier, la versionner et la dupliquer par environnement (voir exercice 5) sans toucher au manifeste de l'application, et la même image sert partout.
Créer des Secrets, comprendre ce que base64 protège (rien), et les consommer avec des permissions restreintes.
Vous maîtrisez les ConfigMaps : les Secrets fonctionnent presque pareil, et c'est justement le piège. À vous de démontrer leurs spécificités.
demo-credentials en ligne de commande avec deux clés : username=admin et password=motdepasse123.api-secrets en YAML (clés api_key et webhook_secret, valeurs libres) sans encoder quoi que ce soit à la main.demo-credentials et décodez-le en une ligne de commande.demo-credentials dans un Pod busybox app-with-secret-files sous /etc/credentials, en lecture seule pour le seul propriétaire, et vérifiez les permissions obtenues.💡 Indices : kubectl create secret generic --from-literal ; stringData évite le base64 manuel ; -o jsonpath='{.data.password}' | base64 -d ; defaultMode: 0400 ; pour vérifier les permissions, ls -laL (les fichiers montés sont des liens symboliques, -L les suit).
1. et 2. Deux méthodes de création :
# Méthode 1 : en ligne de commande
kubectl create secret generic demo-credentials \
--from-literal=username=admin \
--from-literal=password=motdepasse123
Méthode 2 : en YAML, avec stringData (qui évite l'encodage base64 manuel). Créez le fichier api-secrets.yaml :
# api-secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
labels:
app: demo-app
type: Opaque
stringData:
api_key: "sk-1234567890abcdef"
webhook_secret: "mon-secret-webhook"
kubectl apply -f api-secrets.yaml
kubectl get secrets
3. Base64 n'est PAS du chiffrement :
# Le Secret stocké est simplement encodé...
kubectl get secret demo-credentials -o jsonpath='{.data.password}'
echo
# ...et se décode instantanément
kubectl get secret demo-credentials -o jsonpath='{.data.password}' | base64 -d
echo
bW90ZGVwYXNzZTEyMw==
motdepasse123
⚠️ Quiconque peut lire le Secret peut le décoder. La vraie protection vient du RBAC (qui a le droit de lire les Secrets, voir TP 7), du chiffrement etcd au repos, et d'outils dédiés (Vault, Sealed Secrets).
4. Montage en volume avec permissions restreintes :
Fichier app-with-secret-files.yaml :
# app-with-secret-files.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-with-secret-files
spec:
containers:
- name: app
image: busybox:1.36
command: ["sh", "-c"]
args:
- |
echo "=== Secrets montés comme fichiers ==="
ls -laL /etc/credentials/
echo "Username: $(cat /etc/credentials/username)"
sleep 3600
volumeMounts:
- name: demo-secrets
mountPath: /etc/credentials
readOnly: true
volumes:
- name: demo-secrets
secret:
secretName: demo-credentials
defaultMode: 0400
kubectl apply -f app-with-secret-files.yaml
kubectl logs app-with-secret-files
=== Secrets montés comme fichiers ===
total 8
drwxrwxrwt 3 root root 120 Sep 3 09:49 .
drwxr-xr-x 1 root root 70 Sep 3 09:49 ..
drwxr-xr-x 2 root root 80 Sep 3 09:49 ..2026_09_03_09_49_21.3137359859
drwxr-xr-x 2 root root 80 Sep 3 09:49 ..data
-r-------- 1 root root 13 Sep 3 09:49 password
-r-------- 1 root root 5 Sep 3 09:49 username
Username: admin
0400 sur password et username : lecture seule, propriétaire uniquement : principe de moindre privilège.
💡 Pourquoi ls -laL et pas ls -la ? Sans -L, vous verriez password -> ..data/password en lrwxrwxrwx : kubelet monte les Secrets via des liens symboliques vers un répertoire horodaté (le mécanisme qui permet de mettre à jour un Secret de façon atomique). Les permissions 0400 sont posées sur les fichiers cibles, pas sur les liens : -L suit les liens et montre les vraies permissions. Le même mécanisme s'applique aux volumes ConfigMap.
data et stringData dans un Secret ?defaultMode: 0400 pour les volumes de secrets ?Q: Quelle est la différence entre data et stringData dans un Secret ?
R: data exige des valeurs déjà encodées en base64 ; stringData accepte du texte en clair que Kubernetes encode lui-même à la création. À la lecture, tout ressort dans data encodé.
Q: L'encodage base64 protège-t-il réellement les secrets ?
R: Non ! Base64 est de l'encodage, pas du chiffrement, décodable instantanément. La vraie sécurité : RBAC restrictif, chiffrement etcd au repos, rotation, et outils comme Vault ou Sealed Secrets.
Q: Pourquoi définir defaultMode: 0400 pour les volumes de secrets ?
R: Pour restreindre la lecture au seul propriétaire du processus : moindre privilège, et aucune écriture possible même en cas de compromission partielle.
Sortir les identifiants de la base de données des manifestes : depuis le TP 4, POSTGRES_PASSWORD traîne en clair dans le YAML du StatefulSet.
Audit interne : « des identifiants en clair dans des manifestes versionnés ». À corriger, en sachant que les valeurs postgres/postgres restent imposées par le worker et result (codées en dur) : le mécanisme que vous mettez en place est identique avec des identifiants forts.
db-credentials portant les clés POSTGRES_USER et POSTGRES_PASSWORD (valeurs postgres), sans encodage manuel.db pour consommer ce Secret d'un bloc, et supprimez toute valeur d'identifiant en clair du manifeste (PGDATA reste, lui, une simple variable).db-0 recréé, données intactes (sauriez-vous dire pourquoi ?), variables bien issues du Secret ; puis un vote de bout en bout (31000 → 31001).💡 Indices : envFrom: secretRef ; kubectl describe pod db-0 montre la provenance des variables (« Environment Variables from »).
1. Le Secret des identifiants :
Fichier db-credentials-secret.yaml :
# db-credentials-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: voting-app
labels:
app: db
type: Opaque
stringData:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
kubectl apply -f db-credentials-secret.yaml
⚠️ En toute transparence : ces valeurs (postgres/postgres) sont imposées par le worker et result, qui les ont codées en dur. En production, vous mettriez ici des identifiants forts ; le mécanisme que vous apprenez est exactement le même, seules les valeurs changent.
2. Le StatefulSet branché sur le Secret :
Fichier db-statefulset-secret.yaml (le StatefulSet db, sans plus aucun identifiant en clair) :
# db-statefulset-secret.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: db
namespace: voting-app
labels:
app: db
tier: database
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
tier: database
spec:
containers:
- name: postgres
image: postgres:16-alpine
envFrom:
- secretRef:
name: db-credentials
env:
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
ports:
- containerPort: 5432
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 1Gi
kubectl apply -f db-statefulset-secret.yaml
# Le template a changé : db-0 est recréé
kubectl rollout status statefulset/db
3. Vérification - nouveau Pod, mêmes données, chaîne intacte :
# Les données ont survécu à la recréation (merci le PVC du TP 5)
kubectl exec db-0 -- psql -U postgres -c 'SELECT COUNT(*) FROM votes;'
# Les variables viennent bien du Secret
kubectl describe pod db-0 | grep -A3 "Environment Variables from"
count
-------
50
(1 row)
Environment Variables from:
db-credentials Secret Optional: false
Votez une dernière fois sur le port 31000 et vérifiez le résultat sur 31001 : la chaîne complète fonctionne, sans plus aucun mot de passe dans les manifestes applicatifs. ✅
db-0 n'a-t-elle pas perdu de données ?Q: Pourquoi la recréation de db-0 n'a-t-elle pas perdu de données ?
R: Le StatefulSet rattache le nouveau Pod db-0 à son PVC personnel data-db-0 : les données vivent dans le volume, pas dans le Pod. C'est la combinaison des briques des TP 5 et 6.
Q: Quel problème restait-il dans nos manifestes depuis le TP 4, et comment est-il résolu ?
R: POSTGRES_PASSWORD figurait en clair dans le YAML du déploiement (donc dans le git, les revues de code, les sauvegardes…). Il vit désormais dans un Secret dédié, référencé par envFrom.secretRef : les manifestes applicatifs ne contiennent plus de valeur sensible.
Q: Qui peut encore lire le mot de passe, et comment le restreindre ?
R: Toute personne autorisée à lire les Secrets du namespace (kubectl get secret -o yaml + base64 -d). On restreint cela par le RBAC (TP 7), et on renforce avec le chiffrement etcd et des solutions type Vault.
Exploiter la séparation configuration/code : faire tourner un second environnement de l'application de vote, avec les mêmes images mais une configuration différente.
L'équipe produit veut une plateforme de test pour un futur sondage « boissons ». Créez un environnement de développement minimal, à côté de la production (voting-app), avec ses propres options de vote.
voting-devvoting-options avec OPTION_A=Espresso et OPTION_B=Décaféinévote (1 réplica, branché sur la ConfigMap) et un Deployment redis avec son Service redis (un environnement de dev n'a pas besoin de worker ni de base : la file suffit pour tester l'interface)31000 et 31002 dans deux onglets : deux environnements, deux jeux d'options, les mêmes imagesÉtape 1 : Namespace et ConfigMap
Créez le fichier voting-dev.yaml :
# voting-dev.yaml
apiVersion: v1
kind: Namespace
metadata:
name: voting-dev
---
apiVersion: v1
kind: ConfigMap
metadata:
name: voting-options
namespace: voting-dev
data:
OPTION_A: "Espresso"
OPTION_B: "Décaféiné"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vote
namespace: voting-dev
labels:
app: vote
tier: frontend
spec:
replicas: 1
selector:
matchLabels:
app: vote
template:
metadata:
labels:
app: vote
tier: frontend
spec:
containers:
- name: vote
image: dockersamples/examplevotingapp_vote:latest
envFrom:
- configMapRef:
name: voting-options
ports:
- containerPort: 80
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis
namespace: voting-dev
labels:
app: redis
tier: backend
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
tier: backend
spec:
containers:
- name: redis
image: redis:8-alpine
ports:
- containerPort: 6379
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
---
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: voting-dev
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
---
apiVersion: v1
kind: Service
metadata:
name: vote
namespace: voting-dev
spec:
type: NodePort
selector:
app: vote
ports:
- port: 80
targetPort: 80
nodePort: 31002
Étape 2 : Déployer et vérifier
kubectl apply -f voting-dev.yaml
kubectl get pods -n voting-dev
NAME READY STATUS RESTARTS AGE
redis-6f4b8d7c9d-p7zqm 1/1 Running 0 30s
vote-64c8b9f7d6-hx2vn 1/1 Running 0 30s
Étape 3 : Comparer les deux environnements
Ouvrez deux onglets via le Traffic Port Accessor :
31000 : la « production » propose Chats vs Lapins31002 : le « dev » propose Espresso vs Décaféiné# Mêmes images, configurations différentes
kubectl get deployment vote -n voting-app -o jsonpath='{.spec.template.spec.containers[0].image}'
echo
kubectl get deployment vote -n voting-dev -o jsonpath='{.spec.template.spec.containers[0].image}'
echo
dockersamples/examplevotingapp_vote:latest
dockersamples/examplevotingapp_vote:latest
✅ C'est tout le principe de la séparation configuration/code : une seule image, un comportement par environnement, piloté par les ConfigMaps de chaque namespace.
redis (un par namespace) ne se marchent-ils pas dessus ?voting-options manquait dans voting-dev ?Q: Pourquoi les deux Services redis (un par namespace) ne se marchent-ils pas dessus ?
R: Le DNS est namespacé : dans voting-dev, le nom court redis résout vers redis.voting-dev.svc.cluster.local ; dans voting-app, vers redis.voting-app.svc.cluster.local. Chaque vote parle à la file de son environnement.
Q: Que se passerait-il si la ConfigMap voting-options manquait dans voting-dev ?
R: Avec envFrom.configMapRef, les Pods resteraient bloqués en CreateContainerConfigError (ConfigMap introuvable, visible dans kubectl describe pod). Une référence optional: true permettrait de démarrer sans elle, avec les valeurs par défaut de l'application.
vote en « prod » (Chats/Lapins) et en « dev » (Espresso/Décaféiné)envFrom/configMapKeyRef (variables) ou volumes (fichiers)kubectl rollout restart obligatoirevoting-options pilote les intitulés de vote, modifiables sans toucher à l'applicationdb-credentials a sorti le mot de passe PostgreSQL des manifestesvoting-dev prouve la promesse : mêmes images, environnements différentsrollout restart documenté dans les procédures de changement de configuration