45-50 minutes pour le cœur du TP (exercices 1 à 4) + 2 exercices optionnels
💡 Note formateur : les exercices 5 et 6 sont des soupapes, à faire si le groupe est en avance, ou à donner en exercice d'approfondissement.
Ce manifeste reconstruit l'application complète telle que laissée à la fin du TP 6 : les 5 briques, la ConfigMap voting-options et le Secret db-credentials. Appliquez-le dès la première minute.
Créez le fichier fil-rouge-tp07.yaml :
# fil-rouge-tp07.yaml
apiVersion: v1
kind: Namespace
metadata:
name: voting-app
---
apiVersion: v1
kind: ConfigMap
metadata:
name: voting-options
namespace: voting-app
labels:
app: vote
data:
OPTION_A: "Chats"
OPTION_B: "Chiens"
---
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: voting-app
labels:
app: db
type: Opaque
stringData:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
---
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"
---
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
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
---
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-tp07.yaml
kubectl config set-context --current --namespace=voting-app
kubectl wait --for=condition=Available deployment --all --timeout=240s
kubectl get pods
Apprendre à Kubernetes à distinguer un conteneur démarré d'un conteneur réellement prêt, sur chacune de nos briques, avec la sonde adaptée à chaque technologie.
Jour 3 : vous connaissez l'application par cœur. Le tableau ci-dessous est votre spécification : à vous d'écrire les sondes.
| Brique | Readiness / Liveness | Pourquoi |
|---|---|---|
| vote | httpGet sur / port 80 |
Application web : une réponse HTTP prouve qu'elle sert |
| result | httpGet sur / port 80 |
Idem |
| redis | tcpSocket port 6379 |
Pas d'endpoint HTTP : l'ouverture du port TCP suffit |
| db | exec → pg_isready -U postgres |
L'outil officiel PostgreSQL, plus fiable qu'un simple test de port |
| worker | aucune | Aucun port exposé : rien à sonder de l'extérieur (sa « santé » se lit dans ses logs) |
vote, result, redis et db d'une readinessProbe ET d'une livenessProbe conformes au tableau : readiness réactive (délai initial ~5 s), liveness plus patiente (~10-30 s).READY 0/1 avant 1/1.Ready figurent dans les endpoints du Service vote.💡 Indices : trois syntaxes de sonde, httpGet (path + port), tcpSocket (port) et exec (command), combinées à initialDelaySeconds/periodSeconds ; côté endpoints, kubectl describe endpointslice -l kubernetes.io/service-name=vote.
1. Les manifestes équipés de sondes :
Fichier fil-rouge-probes.yaml :
# fil-rouge-probes.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
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
---
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
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
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
readinessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
tcpSocket:
port: 6379
initialDelaySeconds: 10
periodSeconds: 10
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "128Mi"
---
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
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 30
periodSeconds: 10
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 fil-rouge-probes.yaml
# Observer les Pods passer par READY 0/1 avant 1/1
kubectl get pods -w
# (Ctrl+C quand tout est 1/1)
NAME READY STATUS RESTARTS AGE
vote-6b8c9d7f4e-2mkqw 0/1 Running 0 8s
vote-6b8c9d7f4e-2mkqw 1/1 Running 0 12s
💡 Ce 0/1 → 1/1 est nouveau : le conteneur tournait déjà (Running), mais Kubernetes attend désormais le premier succès de la readiness probe pour le déclarer prêt.
3. Seuls les Pods Ready reçoivent du trafic :
# Les sondes déclarées sur un Pod
kubectl describe pod -l app=db | grep -E "Liveness|Readiness"
# L'endpointslice du Service vote ne liste que des Pods "ready: true"
kubectl describe endpointslice -l kubernetes.io/service-name=vote | grep -B1 Ready
✅ Concrètement : pendant un rolling update, un Pod vote fraîchement démarré ne recevra aucune requête tant que sa readiness probe n'a pas réussi. Fini les erreurs servies pendant les déploiements.
exec pg_isready plutôt qu'un tcpSocket pour PostgreSQL ?Q: Quelle est la différence entre liveness et readiness probe ?
R: La readiness décide si le Pod reçoit du trafic (échec → retiré des endpoints du Service, sans redémarrage). La liveness décide si le conteneur doit être redémarré (échec répété → kill + restart). Un Pod peut être vivant mais pas prêt (démarrage, surcharge temporaire).
Q: Pourquoi une sonde exec pg_isready plutôt qu'un tcpSocket pour PostgreSQL ?
R: Le port 5432 peut être ouvert alors que le serveur refuse encore les connexions (démarrage, récupération après crash). pg_isready interroge le protocole PostgreSQL lui-même : c'est l'état applicatif réel.
Q: Que ferait Kubernetes si la liveness probe de redis échouait 3 fois de suite ?
R: Le kubelet tuerait le conteneur et le redémarrerait dans le même Pod (compteur RESTARTS incrémenté). C'est l'auto-réparation au niveau conteneur, complémentaire de celle des Deployments au niveau Pod.
Faire absorber un pic de charge à l'interface de vote automatiquement, avec un Horizontal Pod Autoscaler fondé sur la CPU réellement consommée.
L'HPA a besoin de métriques. Le Metrics Server ne fait pas partie de l'installation standard :
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
# Sur Killercoda (certificats kubelet auto-signés), autoriser le TLS non vérifié :
kubectl patch deployment metrics-server -n kube-system --type='json' \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl wait --for=condition=Available deployment/metrics-server -n kube-system --timeout=120s
# Après ~1 minute, les métriques arrivent :
kubectl top pods
NAME CPU(cores) MEMORY(bytes)
db-0 5m 38Mi
redis-6f4b8d7c9d-x2kqp 3m 4Mi
vote-6b8c9d7f4e-2mkqw 1m 42Mi
...
Le Deployment vote déclare requests.cpu: 100m depuis le TP 3, la référence dont l'HPA a besoin pour calculer un pourcentage :
kubectl autoscale deployment vote --cpu=50% --min=2 --max=5
kubectl get hpa vote
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
vote Deployment/vote cpu: 2%/50% 2 5 3 30s
💡 Au repos, l'HPA pourra descendre à 2 réplicas (notre --min) : il gère désormais le champ replicas à la place de l'humain.
# Un générateur de charge qui martèle le Service vote
kubectl run load-generator --image=busybox:1.36 --restart=Never -- sh -c "while true; do wget -q -O- http://vote > /dev/null; done"
# Observer l'HPA réagir (Ctrl+C pour arrêter l'observation)
kubectl get hpa vote -w
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
vote Deployment/vote cpu: 2%/50% 2 5 3 2m
vote Deployment/vote cpu: 68%/50% 2 5 3 3m
vote Deployment/vote cpu: 68%/50% 2 5 5 3m15s
vote Deployment/vote cpu: 41%/50% 2 5 5 4m
💡 Si TARGETS monte à peine, lancez un second générateur (load-generator-2) : la page de vote est légère, un seul busybox ne sature pas toujours 3 réplicas.
kubectl delete pod load-generator --ignore-not-found
kubectl delete pod load-generator-2 --ignore-not-found
kubectl get hpa vote -w
Le retour à 2 réplicas prend environ 5 minutes : la fenêtre de stabilisation évite les oscillations (un pic → des Pods créés → charge répartie → CPU redescend → Pods supprimés → CPU remonte…).
requests.cpu sur le Deployment ?maxReplicas ?Q: Pourquoi l'HPA a-t-il besoin de requests.cpu sur le Deployment ?
R: La cible (50 %) est un pourcentage des requests : consommation réelle / requests. Sans requests CPU, aucun pourcentage n'est calculable et l'HPA affiche <unknown>.
Q: Pourquoi le scale-down est-il volontairement plus lent que le scale-up ?
R: Scale-up rapide pour absorber les pics ; scale-down stabilisé (5 min par défaut) pour éviter les oscillations quand la charge fluctue autour du seuil.
Q: Que se passe-t-il si la charge dépasse la capacité de maxReplicas ?
R: L'HPA plafonne à maxReplicas (5 ici), quoi qu'il arrive. Au-delà, il faut relever la limite, optimiser l'application ou ajouter des nœuds : l'autoscaling horizontal des Pods ne crée pas de capacité machine.
Donner à la développeuse Alice un accès limité au namespace voting-app, avec la procédure complète : certificat X.509, permissions, et un vrai contexte kubectl à son nom.
# Clé privée
openssl genrsa -out alice.key 2048
# Demande de certificat (CN = nom d'utilisateur, O = groupe)
openssl req -new -key alice.key -out alice.csr -subj "/CN=alice/O=developers"
# La demande, encodée pour l'API Kubernetes
cat << EOF > alice-csr.yaml
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: alice-csr
spec:
request: $(cat alice.csr | base64 | tr -d '\n')
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 31536000 # 1 an
usages:
- client auth
EOF
kubectl apply -f alice-csr.yaml
kubectl get csr alice-csr
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
alice-csr 5s kubernetes.io/kube-apiserver-client kubernetes-admin 365d Pending
kubectl certificate approve alice-csr
# Extraire le certificat signé par la CA du cluster
kubectl get csr alice-csr -o jsonpath='{.status.certificate}' | base64 -d > alice.crt
openssl x509 -in alice.crt -noout -subject -issuer
subject=O = developers, CN = alice
issuer=CN = kubernetes
Créez le fichier alice-rbac.yaml :
# alice-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: voting-app
name: developer-role
rules:
# Lecture des Pods et de leurs logs
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# Lecture des Services et ConfigMaps
- apiGroups: [""]
resources: ["services", "configmaps"]
verbs: ["get", "list", "watch"]
# Gestion des Deployments (mais pas de suppression)
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "update", "patch"]
# Volontairement absents : secrets, delete, autres namespaces
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: voting-app
subjects:
- kind: User
name: alice # le CN du certificat
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer-role
apiGroup: rbac.authorization.k8s.io
kubectl apply -f alice-rbac.yaml
# Pré-vérification rapide des permissions
kubectl auth can-i list pods --as=alice -n voting-app
kubectl auth can-i get secrets --as=alice -n voting-app
kubectl auth can-i list pods --as=alice -n kube-system
yes
no
no
--as simule ; un vrai utilisateur a son certificat dans son contexte kubectl :
# Enregistrer l'identité d'Alice (certificat + clé embarqués)
kubectl config set-credentials alice --client-certificate=alice.crt --client-key=alice.key --embed-certs=true
# Créer son contexte, pointant sur le même cluster
kubectl config set-context alice --cluster=$(kubectl config view -o jsonpath='{.clusters[0].name}') --user=alice --namespace=voting-app
kubectl config get-contexts
# Ce qu'Alice PEUT faire
kubectl --context=alice get pods
kubectl --context=alice logs deployment/worker --tail=3
# Ce qu'Alice NE PEUT PAS faire
kubectl --context=alice get secrets
Error from server (Forbidden): secrets is forbidden: User "alice" cannot list resource "secrets" in API group "" in the namespace "voting-app"
kubectl --context=alice get pods -n kube-system
Error from server (Forbidden): pods is forbidden: User "alice" cannot list resource "pods" in API group "" in the namespace "kube-system"
✅ Alice voit l'application, lit les logs, peut modifier les Deployments, mais le mot de passe de la base (le Secret du TP 6) et le reste du cluster lui sont fermés. C'est la protection réelle des Secrets évoquée au TP 6.
Q: Quelle est la différence entre User et ServiceAccount dans RBAC ?
R: Un User est une identité humaine, authentifiée à l'extérieur du cluster (certificat X.509, OIDC) ; Kubernetes ne stocke aucun objet « User ». Un ServiceAccount est une identité d'application, créée dans le cluster et injectée dans les Pods sous forme de token (utilisé au TP 8).
Q: Où le nom d'utilisateur « alice » est-il réellement défini ?
R: Dans le CN du certificat (/CN=alice), signé par la CA du cluster. Le RoleBinding référence ce nom : c'est la correspondance CN ↔ subject qui donne les droits. Le champ O (developers) définit son groupe, également utilisable dans un binding.
Q: Pourquoi un Role plutôt qu'un ClusterRole ici ?
R: Le Role est limité à voting-app : Alice n'a aucune existence ailleurs (démontré par le Forbidden sur kube-system). Principe de moindre privilège : un ClusterRole ne se justifie que pour des permissions transverses ou des ressources non namespacées.
Verrouiller les flux réseau du fil rouge : par défaut, tout Pod du cluster peut se connecter à notre base de données. Après cet exercice, seuls les flux légitimes passeront.
Audit de sécurité : « n'importe quel Pod compromis dans le cluster peut lire votre base de votes ». Prouvez-le, puis corrigez-le.
default, connectez-vous au port 5432 de db.voting-app : ça passe.default-deny-ingress qui bloque tout trafic entrant vers tous les Pods de voting-app.vote et result doivent rester joignables depuis l'extérieur (NodePorts 31000/31001). ⚠️ Le trafic NodePort vient de l'extérieur du cluster : il n'est pas identifiable par un podSelector, il faut une règle « ingress ouvert » (ingress: [{}]) sur ces deux composantsredis n'accepte que vote et worker, sur le port 6379db n'accepte que worker et result, sur le port 5432💡 Note : l'application des Network Policies dépend du plugin réseau (CNI) du cluster. Le playground Killercoda embarque un CNI qui les applique ; si vos tests « bloqués » passent quand même, votre CNI ne les applique pas.
1. Prouver le problème
kubectl run intruder -n default --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl exec -n default intruder -- nc -zv -w 2 db.voting-app 5432
db.voting-app (10.111.45.12:5432) open
N'importe quel Pod du cluster atteint la base.
2. et 3. Les quatre Network Policies
Créez le fichier voting-app-netpol.yaml :
# voting-app-netpol.yaml
# Verrou global : plus aucun trafic entrant par défaut
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: voting-app
spec:
podSelector: {}
policyTypes:
- Ingress
---
# vote et result : joignables depuis l'extérieur (NodePort)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-vote-external
namespace: voting-app
spec:
podSelector:
matchLabels:
app: vote
policyTypes:
- Ingress
ingress:
- {} # tout trafic entrant autorisé vers vote
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-result-external
namespace: voting-app
spec:
podSelector:
matchLabels:
app: result
policyTypes:
- Ingress
ingress:
- {} # tout trafic entrant autorisé vers result
---
# redis : seulement vote et worker, port 6379
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-redis-from-app
namespace: voting-app
spec:
podSelector:
matchLabels:
app: redis
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: vote
- podSelector:
matchLabels:
app: worker
ports:
- protocol: TCP
port: 6379
---
# db : seulement worker et result, port 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db-from-app
namespace: voting-app
spec:
podSelector:
matchLabels:
app: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: worker
- podSelector:
matchLabels:
app: result
ports:
- protocol: TCP
port: 5432
kubectl apply -f voting-app-netpol.yaml
kubectl get networkpolicy -n voting-app
NAME POD-SELECTOR AGE
allow-db-from-app app=db 10s
allow-redis-from-app app=redis 10s
allow-result-external app=result 10s
allow-vote-external app=vote 10s
default-deny-ingress <none> 10s
4. Tester
# L'intrus est désormais bloqué (timeout après 2 secondes)
kubectl exec -n default intruder -- nc -zv -w 2 db.voting-app 5432
nc: db.voting-app (10.111.45.12:5432): Connection timed out
# Même un Pod DU namespace, sans le bon label, est bloqué
kubectl run intruder-interne -n voting-app --image=busybox:1.36 --restart=Never --rm -it -- nc -zv -w 2 redis 6379
nc: redis (10.108.22.190:6379): Connection timed out
Enfin, le test qui compte : ouvrez les ports 31000 et 31001, votez, vérifiez le résultat. La chaîne légitime, elle, fonctionne toujours (vote → redis : autorisé ; worker → redis et db : autorisé ; result → db : autorisé).
kubectl delete pod intruder -n default
ingress: [{}] pour vote et result plutôt qu'un podSelector ?default-deny-ingress, et que ne bloque-t-elle pas ?Q: Pourquoi ingress: [{}] pour vote et result plutôt qu'un podSelector ?
R: Le trafic NodePort provient de l'extérieur du cluster (votre navigateur) : il n'émane d'aucun Pod, donc aucun podSelector ne peut le décrire. La règle vide {} autorise tout trafic entrant vers ces Pods, ce qui est acceptable pour des frontaux publics.
Q: Que bloque exactement default-deny-ingress, et que ne bloque-t-elle pas ?
R: Elle bloque tout trafic entrant vers tous les Pods du namespace voting-app (sauf ré-autorisation par une autre policy). Elle ne bloque pas le trafic sortant (egress) : vote peut toujours initier sa connexion vers redis ; c'est le retour de connexion qui est évalué côté redis, en ingress.
Q: Pourquoi le worker n'a-t-il besoin d'aucune règle d'ingress ?
R: Personne ne se connecte au worker (pas de port, pas de Service). Le deny-all en ingress ne le gêne donc pas, et ses connexions sortantes vers redis et db sont couvertes par les règles d'ingress de ces derniers.
Aller au bout du zero-trust : contrôler aussi le trafic sortant. Un Pod compromis dans voting-app ne doit pas pouvoir exfiltrer de données.
httpbin.org).💡 Indice : le flux vital de l'étape 2 est la résolution DNS (port 53 UDP/TCP vers le namespace kube-system).
1. à 3. Le jeu de policies egress :
Fichier voting-app-netpol-egress.yaml :
# voting-app-netpol-egress.yaml
# Verrou global ingress + egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: voting-app
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Indispensable après un deny egress : la résolution DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: voting-app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# vote -> redis
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-vote-egress
namespace: voting-app
spec:
podSelector:
matchLabels:
app: vote
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: redis
ports:
- protocol: TCP
port: 6379
---
# worker -> redis + db
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-worker-egress
namespace: voting-app
spec:
podSelector:
matchLabels:
app: worker
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: redis
ports:
- protocol: TCP
port: 6379
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432
---
# result -> db
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-result-egress
namespace: voting-app
spec:
podSelector:
matchLabels:
app: result
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: db
ports:
- protocol: TCP
port: 5432
kubectl apply -f voting-app-netpol-egress.yaml
4. Vérification :
# La chaîne légitime fonctionne toujours : votez sur 31000, vérifiez sur 31001
# Mais un Pod de voting-app ne peut plus sortir vers l'extérieur
kubectl run exfiltration-test -n voting-app --image=busybox:1.36 --restart=Never --rm -it -- wget -q -T 3 -O- http://httpbin.org/ip
wget: download timed out
💡 Sans la policy allow-dns, tout casserait : les applications ne pourraient même plus résoudre redis ou db. C'est l'oubli classique du zero-trust ; le TP 8 réutilisera ce jeu complet de policies.
Apprendre à contrôler où se déploient les Pods avec les Taints et Tolerations.
Vous avez un cluster avec des nodes spécialisés. Certains nodes doivent être réservés pour des applications spécifiques (bases de données, applications critiques, etc.).
Implémentez un placement contrôlé des Pods :
Appliquer une Taint sur un node :
Tester le comportement par défaut :
Créer un Pod avec tolérance :
Nettoyer :
1. Appliquer une taint sur un node worker
# Lister les nodes
kubectl get nodes
# Identifier le node worker (pas le control-plane)
# Appliquer une taint "database=true:NoSchedule"
kubectl taint node node01 database=true:NoSchedule
# Vérifier la taint appliquée
kubectl describe node node01 | grep Taints
2. Tester le comportement par défaut
Créez le fichier app-normal.yaml :
# app-normal.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-normal
namespace: default
labels:
type: normal
spec:
containers:
- name: nginx
image: nginx:1.29.5-alpine
ports:
- containerPort: 80
Déployez et observez :
# Déployer le pod normal
kubectl apply -f app-normal.yaml
# Observer qu'il reste en Pending
kubectl get pods -n default app-normal
echo "Le Pod reste en Pending car il n'a pas la tolérance pour la taint"
# Voir les détails (Events)
kubectl describe pod -n default app-normal | grep -A5 Events
3. Créer un Pod avec tolérance
Créez le fichier app-database.yaml :
# app-database.yaml
apiVersion: v1
kind: Pod
metadata:
name: app-database
namespace: default
labels:
type: database
spec:
# Tolérance pour la taint "database"
tolerations:
- key: "database"
operator: "Equal"
value: "true"
effect: "NoSchedule"
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_PASSWORD
value: "mysecretpassword"
ports:
- containerPort: 5432
Déployez et vérifiez :
# Déployer le pod avec tolérance
kubectl apply -f app-database.yaml
# Vérifier qu'il se déploie correctement
kubectl get pods -n default app-database -o wide
echo "Le Pod se déploie car il a la tolérance pour la taint"
4. Nettoyer
# Retirer la taint du node (ajouter un '-' à la fin)
kubectl taint node node01 database=true:NoSchedule-
# Vérifier que la taint a été retirée
kubectl describe node node01 | grep Taints
# Le pod normal devrait maintenant se déployer
kubectl get pods -n default -o wide
# Supprimer les pods de test
kubectl delete pod -n default app-normal app-database
NoSchedule et NoExecute ?Q: Que se passe-t-il quand un Pod n'a pas la tolérance pour une taint ?
R: Le Pod reste en état "Pending" car le scheduler ne peut pas le placer sur un node avec une taint. Le Pod attendra indéfiniment jusqu'à ce qu'un node sans cette taint soit disponible ou que la taint soit retirée.
Q: Quelle est la différence entre les effets NoSchedule et NoExecute ?
R:
Q: Comment retirer une taint d'un node ?
R: En ajoutant un - à la fin de la commande taint : kubectl taint node <node-name> key=value:effect-. Par exemple : kubectl taint node node01 database=true:NoSchedule-
httpGet pour le web, tcpSocket faute de mieux, exec avec l'outil natif (pg_isready) pour les basesrequests.cpu sur les conteneurs + Metrics Server (--kubelet-insecure-tls sur les clusters de lab)replicas : ne plus le fixer à la main sur un Deployment autoscalékubectl auth can-i pour pré-vérifier, le vrai contexte pour la démonstrationdb-credentialsingress: [{}] : le trafic externe n'est pas identifiable par podSelector