45 minutes (+ 2 exercices optionnels)
Ce manifeste reconstruit l'état du fil rouge à la fin du TP 3 : le namespace voting-app et les Deployments vote (3 réplicas) et redis.
⚠️ Le Service redis est volontairement absent : vous allez le recréer proprement, et comprendre pourquoi son nom n'est pas négociable, à l'exercice 2.
Créez le fichier fil-rouge-tp04.yaml :
# fil-rouge-tp04.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"
Puis appliquez-le et placez-vous dans le namespace :
kubectl apply -f fil-rouge-tp04.yaml
kubectl config set-context --current --namespace=voting-app
kubectl wait --for=condition=Available deployment --all --timeout=180s
kubectl get pods
NAME READY STATUS RESTARTS AGE
redis-6f4b8d7c9d-x2kqp 1/1 Running 0 60s
vote-7d94f6c8b5-4x8mn 1/1 Running 0 60s
vote-7d94f6c8b5-jw8tn 1/1 Running 0 60s
vote-7d94f6c8b5-qp2rk 1/1 Running 0 60s
Constater sur nos propres Pods pourquoi on ne peut pas se fier aux IPs.
# Les IPs actuelles de nos Pods
kubectl get pods -o wide
# Depuis un Pod de test, joindre un Pod vote par son IP
# Remplacez <IP_POD_VOTE> par une IP relevée ci-dessus
kubectl run test-ip --image=busybox:1.36 -it --rm -- sh
# Dans le Pod de test :
# wget -qO- http://<IP_POD_VOTE> | head -3
# exit
# Supprimer le Pod vote correspondant (remplacez <NOM_POD_VOTE>)
kubectl delete pod <NOM_POD_VOTE>
# Le Deployment l'a remplacé... avec une NOUVELLE IP
kubectl get pods -o wide
❌ L'ancienne IP ne répond plus : toute configuration qui la référençait est cassée. Et avec 3 réplicas, quelle IP faudrait-il choisir de toute façon ?
✅ La réponse à ces deux problèmes (stabilité et multiplicité), c'est le Service.
Q: Pourquoi ne peut-on pas communiquer directement par IP entre Pods ?
R: Les IPs des Pods changent à chaque recréation (panne, rolling update, scaling). Toute communication fondée sur une IP de Pod se casse tôt ou tard.
Q: Quel second problème pose un Deployment à plusieurs réplicas ?
R: Il y a plusieurs IPs pour un même rôle : il faut un point d'entrée unique qui répartisse le trafic entre les réplicas. C'est exactement ce que fait un Service.
redis - le Nom Fait Tout (Guidé)Comprendre que le nom d'un Service est un contrat avec les applications : l'application de vote cherche l'hôte redis, ni plus ni moins.
Votre collègue a créé le Service avec une convention de nommage maison. Créez d'abord le fichier redis-svc.yaml :
# redis-svc.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-svc
namespace: voting-app
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Puis appliquez-le :
kubectl apply -f redis-svc.yaml
kubectl get service redis-svc
Le Service existe, il a bien trouvé le Pod redis (vérifiez avec kubectl describe service redis-svc : la section Endpoints n'est pas vide). Tout semble en ordre…
kubectl port-forward deployment/vote 8080:80 --address 0.0.0.0
Ouvrez le port 8080 (menu ☰ → Traffic / Ports) et tentez de voter : la page se charge, mais le vote renvoie une erreur 500. Arrêtez le port-forward (Ctrl+C) et regardez les logs :
kubectl logs deployment/vote --tail=15
[2026-08-13 09:12:44,301] ERROR in app: Exception on / [POST]
Traceback (most recent call last):
...
redis.exceptions.ConnectionError: Error -2 connecting to redis:6379. Name does not resolve.
💡 Tout est dit : l'application cherche l'hôte redis, un nom codé en dur dans son code. Notre Service s'appelle redis-svc : pour l'application, il n'existe pas.
kubectl run dns-test --image=busybox:1.36 -it --rm -- sh
# Dans le Pod de test :
# nslookup redis-svc -> répond (le Service existe)
# nslookup redis -> NXDOMAIN (ce nom n'existe pas)
# exit
redisCréez le fichier redis-service.yaml :
# redis-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
namespace: voting-app
labels:
app: redis
spec:
selector:
app: redis
ports:
- port: 6379
targetPort: 6379
Puis remplacez l'ancien Service :
kubectl delete service redis-svc
kubectl apply -f redis-service.yaml
# Revotez pour confirmer (port-forward + navigateur), puis :
kubectl exec deployment/redis -- redis-cli LLEN votes
(integer) 1
✅ Le vote fonctionne : en Kubernetes, le nom du Service EST l'adresse de l'application. C'est le contrat d'interface entre vos composants.
# Le nom complet fonctionne aussi (et depuis n'importe quel namespace)
kubectl run dns-test2 --image=busybox:1.36 -it --rm -- sh
# Dans le Pod de test :
# nslookup redis
# nslookup redis.voting-app.svc.cluster.local
# exit
# La table de correspondance nom -> IPs de Pods
kubectl get endpointslice -l kubernetes.io/service-name=redis
redis ?Q: Pourquoi le Service devait-il impérativement s'appeler redis ?
R: Parce que l'application de vote a ce nom d'hôte codé en dur. Le nom d'un Service devient un enregistrement DNS : c'est l'adresse que les autres composants utilisent. Nommer ses Services comme les applications les attendent est un vrai sujet d'exploitation.
Q: Comment un Service maintient-il la liste des Pods joignables ?
R: Via son sélecteur de labels : un contrôleur met à jour en continu l'endpointslice du Service avec les IP:port des Pods correspondants et prêts. Le trafic n'est routé que vers ces endpoints.
Q: Quel est le format DNS complet d'un Service ?
R: <service>.<namespace>.svc.cluster.local (ex. redis.voting-app.svc.cluster.local). Dans le même namespace, le nom court suffit.
Compléter la chaîne de traitement : une base PostgreSQL nommée db, puis le worker qui déverse enfin la file des votes dans la base.
Vous avez écrit des Deployments et des Services toute la matinée : cette fois, la spécification suffit. Deux briques à poser, un déclic à la clé.
db (image postgres:16-alpine, variables POSTGRES_USER et POSTGRES_PASSWORD à postgres, port 5432, labels app: db / tier: database, requests 128Mi/100m, limite 256Mi). Sans volume ni Secret : ces deux dettes sont assumées, elles seront réglées aux TP 5 et 6.db.seed-votes du TP 3, puis vérifiez que la file contient bien 50 votes.worker (image dockersamples/examplevotingapp_worker:latest, labels app: worker / tier: backend, requests 128Mi/100m, limite mémoire 512Mi), sans port ni Service. Pourquoi, à votre avis ?LLEN → 0) et les votes apparaissent dans PostgreSQL.💡 Indices : 1-2 RESTARTS du worker sont normaux le temps que ses dépendances répondent ; côté SQL : kubectl exec deployment/db -- psql -U postgres -c 'SELECT vote, COUNT(*) FROM votes GROUP BY vote;'.
1. et 2. PostgreSQL et son Service db :
Fichier db-deployment.yaml :
# db-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: voting-app
labels:
app: db
tier: database
spec:
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"
ports:
- containerPort: 5432
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
⚠️ Deux choix assumés, corrigés dans les prochains TP : aucun volume (les données vivent dans le conteneur, voir TP 5) et mot de passe en clair dans le manifeste (TP 6).
Le worker et l'application de résultats cherchent la base sous le nom d'hôte db : vous connaissez la règle. Créez le fichier db-service.yaml :
# db-service.yaml
apiVersion: v1
kind: Service
metadata:
name: db
namespace: voting-app
labels:
app: db
spec:
selector:
app: db
ports:
- port: 5432
targetPort: 5432
Puis déployez :
kubectl apply -f db-deployment.yaml -f db-service.yaml
kubectl get pods -l app=db
3. Remplir la file de votes :
Rejouez le Job de seed du TP 3 (recréez seed-votes-job.yaml si nécessaire) :
# seed-votes-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: seed-votes
namespace: voting-app
spec:
backoffLimit: 3
template:
spec:
restartPolicy: Never
containers:
- name: seed
image: redis:8-alpine
command: ["sh", "-c"]
args:
- |
for i in $(seq 1 50); do
redis-cli -h redis RPUSH votes "{\"voter_id\": \"seed-$i\", \"vote\": \"a\"}" > /dev/null
done
echo "✅ 50 votes injectés dans la file"
kubectl apply -f seed-votes-job.yaml
kubectl wait --for=condition=Complete job/seed-votes --timeout=60s
# La file est pleine, personne ne consomme
kubectl exec deployment/redis -- redis-cli LLEN votes
(integer) 50
4. Le chaînon manquant - le worker :
Fichier worker-deployment.yaml :
# worker-deployment.yaml
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"
💡 Remarquez : aucun port, et il n'aura jamais de Service. Le worker est un pur consommateur : c'est lui qui se connecte à redis et db, personne ne se connecte à lui.
kubectl apply -f worker-deployment.yaml
kubectl get pods -l app=worker
NAME READY STATUS RESTARTS AGE
worker-5b8f9d6c47-t8slk 1/1 Running 1 (18s ago) 45s
⚠️ Un ou deux RESTARTS sont normaux : le worker plante tant qu'il n'a pas trouvé ses deux dépendances, et Kubernetes le relance jusqu'à ce que ça passe.
5. Le déclic - la file se vide :
kubectl logs deployment/worker --tail=5
kubectl exec deployment/redis -- redis-cli LLEN votes
Processed vote for 'a' by 'seed-49'
Processed vote for 'a' by 'seed-50'
(integer) 0
Les 50 votes ont été consommés. Où sont-ils passés ?
kubectl exec deployment/db -- psql -U postgres -c 'SELECT vote, COUNT(*) FROM votes GROUP BY vote;'
vote | count
------+-------
a | 50
b | 1
(2 rows)
✅ La chaîne complète fonctionne : navigateur → vote → redis → worker → PostgreSQL. Chaque maillon a trouvé le suivant uniquement grâce aux noms DNS des Services.
Q: Pourquoi le worker n'a-t-il pas besoin de Service ?
R: Un Service sert à recevoir du trafic. Le worker n'écoute sur aucun port : il initie toutes ses connexions (vers redis et db). Rien ne cherche à le joindre, donc rien à exposer.
Q: Par quels noms DNS le worker a-t-il trouvé la file et la base ?
R: redis et db, les noms de nos deux Services ClusterIP ; ces noms d'hôtes sont codés en dur dans le worker, comme dans les autres composants de l'application.
Q: Pourquoi les redémarrages initiaux du worker ne sont-ils pas graves ?
R: C'est le modèle Kubernetes : chaque composant démarre quand il peut, échoue s'il manque une dépendance, et est relancé jusqu'à convergence. L'ordre de démarrage n'a pas besoin d'être orchestré manuellement.
Exposer l'interface de vote hors du cluster et voir, dans le navigateur, le trafic se répartir entre les 3 réplicas.
L'application fonctionne… mais seuls les Pods du cluster peuvent voter. Il est temps d'ouvrir les portes, sur le port convenu avec l'équipe : 31000.
vote avec un Service NodePort nommé vote, en YAML déclaratif : port 80, NodePort fixé à 31000 (pas de port aléatoire : c'est la convention documentée du fil rouge).curl que les 3 réplicas répondent à tour de rôle.💡 Indices : le champ nodePort se fixe dans spec.ports ; le pied de page de l'interface affiche Processed by container ID … ; grep -o 'vote-[a-z0-9]*-[a-z0-9]*' extrait le nom du Pod.
1. Le Service NodePort :
Fichier vote-service.yaml :
# vote-service.yaml
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
Puis appliquez et vérifiez :
kubectl apply -f vote-service.yaml
kubectl get service vote
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
vote NodePort 10.99.141.72 <none> 80:31000/TCP 10s
2. Accès depuis l'extérieur :
Le port 31000 est maintenant ouvert sur chaque nœud du cluster. Sur Killercoda : menu ☰ → Traffic / Ports → port 31000. Votez !
3. Le load balancing à l'œil nu :
Regardez le pied de page de l'interface : Processed by container ID vote-xxxxx. Rechargez la page plusieurs fois : le nom change, chaque requête pouvant atterrir sur un réplica différent.
La même chose en ligne de commande :
for i in $(seq 1 6); do curl -s localhost:31000 | grep -o 'vote-[a-z0-9]*-[a-z0-9]*' | head -1; done
vote-7d94f6c8b5-4x8mn
vote-7d94f6c8b5-qp2rk
vote-7d94f6c8b5-4x8mn
vote-7d94f6c8b5-jw8tn
vote-7d94f6c8b5-qp2rk
vote-7d94f6c8b5-jw8tn
✅ Les 3 réplicas répondent à tour de rôle : le Service répartit le trafic entre tous les Pods sains de son sélecteur, sans aucune configuration supplémentaire.
Q: Quelle est la différence entre ClusterIP et NodePort ?
R: ClusterIP (par défaut) n'est joignable que depuis l'intérieur du cluster, parfait pour redis et db. NodePort ouvre en plus un port (30000-32767) sur chaque nœud, rendant le Service joignable de l'extérieur via <IP-du-nœud>:<port>.
Q: Dans quelle plage se situent les NodePorts, et pourquoi avons-nous fixé 31000 ?
R: 30000-32767 par défaut. Fixer nodePort: 31000 (plutôt que laisser Kubernetes en choisir un au hasard) rend l'accès prévisible et documentable : c'est la convention retenue pour le fil rouge (31000 vote, 31001 result).
Q: Comment le Service choisit-il le Pod destinataire d'une requête ?
R: kube-proxy répartit les connexions entre les endpoints sains du Service (répartition aléatoire/round-robin selon le mode). Seuls les Pods Ready reçoivent du trafic.
Apprendre à diagnostiquer et résoudre les problèmes de réseau les plus courants avec les Services.
Cet exercice se déroule hors du fil rouge : revenez d'abord dans le namespace default :
kubectl config set-context --current --namespace=default
Votre équipe vous signale que l'application ne fonctionne plus correctement. Identifiez et corrigez les problèmes dans cette architecture :
# Configuration problématique
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: web-frontend
spec:
containers:
- name: nginx
image: nginx:1.29.5
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: webapp-service
spec:
type: ClusterIP
selector:
app: webapp-frontend
ports:
- port: 80
targetPort: 80
Mission :
kubectl describe et kubectl get endpointslice pour diagnostiquerkubectl get endpointslice pour voir si le Service trouve ses PodsDiagnostic des problèmes :
Problème 0 - Le Deployment lui-même est invalide :
kubectl apply -f configuration.yaml
# Le Deployment est rejeté : "selector does not match template labels"
# (selector app=webapp vs labels du template app=web-frontend)
Problème 1 - Sélecteur du Service incohérent :
# Une fois le Deployment corrigé (labels app=webapp), le Service
# cherche encore app=webapp-frontend
kubectl describe service webapp-service
# Montre : "Endpoints: <none>" (aucun Pod trouvé)
Problème 2 - Ports incorrects :
# Le conteneur nginx écoute sur 80, mais le template annonce 8080
kubectl describe pod <pod-name>
# Montre : containerPort: 8080
Configuration corrigée :
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
spec:
replicas: 3
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
containers:
- name: nginx
image: nginx:1.29.5
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: webapp-service
spec:
type: ClusterIP
selector:
app: webapp
ports:
- port: 80
targetPort: 80
Commandes de diagnostic :
# Vérifier les endpoints du Service
kubectl get endpointslice -l kubernetes.io/service-name=webapp-service
# Tester la connectivité
kubectl run test-pod --image=busybox:1.36 -it --rm -- sh
# Dans le Pod : wget -qO- http://webapp-service
# Vérifier les labels
kubectl get pods --show-labels
kubectl describe service webapp-service
Q: Comment diagnostiquer un Service qui ne trouve pas ses Pods ?
R:
kubectl get endpointslice -l kubernetes.io/service-name=<service> → Vérifier si des Pods sont listéskubectl describe service <service> → Comparer les selectorskubectl get pods --show-labels → Vérifier les labels des PodsQ: Que signifie "Endpoints: <none>" dans un Service ?
R: Le Service ne trouve aucun Pod correspondant à ses selectors. Il faut vérifier que les labels des Pods correspondent exactement aux selectors du Service.
Q: Comment vérifier que les ports sont correctement configurés ?
R:
service.spec.ports.targetPort avec container.ports.containerPortkubectl describe pod pour voir les ports exposésDonner un nom DNS interne stable à un service situé hors du cluster.
httpbin-external de type ExternalName pointant vers httpbin.org (port 80).http://httpbin-external/json : le nom interne mène au service externe.1. Le Service ExternalName :
Fichier httpbin-external.yaml :
# httpbin-external.yaml
apiVersion: v1
kind: Service
metadata:
name: httpbin-external
spec:
type: ExternalName
externalName: httpbin.org
ports:
- port: 80
Puis créez le Service :
kubectl apply -f httpbin-external.yaml
# Vérifier le Service ExternalName
kubectl get service httpbin-external
kubectl describe service httpbin-external
# Tester l'accès au service externe via DNS interne
kubectl run test-external --image=busybox:1.36 -it --rm -- sh
# Dans le Pod de test :
# wget -qO- http://httpbin-external/json
# exit
💡 Cas d'usage : donner un nom interne stable à une base de données managée ou une API tierce, ce qui facilitera une future migration vers le cluster (il suffira de remplacer l'ExternalName par un vrai Service, sans toucher aux applications).
Avant de partir, une dernière expérience. Replacez-vous dans le fil rouge et supprimez le Pod de la base :
kubectl config set-context --current --namespace=voting-app
kubectl delete pod -l app=db
# Le Deployment recrée un Pod immédiatement...
kubectl get pods -l app=db
# ...mais nos votes ?
kubectl exec deployment/db -- psql -U postgres -c 'SELECT COUNT(*) FROM votes;'
count
-------
0
(1 row)
❌ Tout est perdu. Le Pod est revenu (merci le Deployment), le Service db route à nouveau (merci la Service Discovery)… mais les données vivaient dans le conteneur, et le conteneur est mort. (Si la commande affiche relation "votes" does not exist, c'est le même constat : la table réapparaîtra, vide, quand le worker se reconnectera.)
Rendez-vous au TP 5 pour donner à cette base une vraie mémoire.
<service>.<namespace>.svc.cluster.localredis, worker et result cherchent db