Skip to content

TP : Déployer l'application Guestbook

Ce tutoriel pratique vous guide pas à pas dans le déploiement d'une application web complète sur Kubernetes. Vous allez découvrir comment les différents objets Kubernetes s'articulent ensemble.

Présentation de l'application

Guestbook est une application web classique composée de :

  • Un frontend en Go qui sert une interface web
  • Un backend Redis pour stocker les messages
┌─────────────────────────────────────────────────────────────┐
│                      Navigateur Web                          │
│                            │                                 │
│                            ▼                                 │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                   Service frontend                       ││
│  │                   (LoadBalancer:3000)                    ││
│  └─────────────────────────────────────────────────────────┘│
│                            │                                 │
│              ┌─────────────┼─────────────┐                  │
│              ▼             ▼             ▼                  │
│  ┌───────────────┐ ┌───────────────┐ ┌───────────────┐     │
│  │  guestbook-1  │ │  guestbook-2  │ │  guestbook-3  │     │
│  │   (Go app)    │ │   (Go app)    │ │   (Go app)    │     │
│  └───────────────┘ └───────────────┘ └───────────────┘     │
│              │             │             │                  │
│              └─────────────┼─────────────┘                  │
│                            ▼                                 │
│  ┌─────────────────────────────────────────────────────────┐│
│  │                    Service redis                         ││
│  │                   (ClusterIP:6379)                       ││
│  └─────────────────────────────────────────────────────────┘│
│                            │                                 │
│                            ▼                                 │
│                    ┌───────────────┐                        │
│                    │     redis     │                        │
│                    │   (backend)   │                        │
│                    └───────────────┘                        │
└─────────────────────────────────────────────────────────────┘

Fonctionnement

  1. L'utilisateur accède au frontend via son navigateur
  2. Le frontend affiche un formulaire pour laisser un message
  3. Les messages sont stockés dans Redis
  4. Le frontend poll Redis toutes les secondes pour afficher les nouveaux messages

Partie 1 : Les Pods simples

Commençons par comprendre les briques de base avant de passer aux Deployments.

1.1 Le Pod Redis

yaml
# k8s-resources/redis-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: redis
  labels:
    app: redis
    tier: backend
spec:
  containers:
  - name: redis
    image: redis
    ports:
    - containerPort: 6379

Analyse ligne par ligne

LigneExplication
apiVersion: v1Version de l'API Kubernetes. v1 est le core API, utilisé pour les objets de base (Pod, Service, ConfigMap).
kind: PodType d'objet Kubernetes à créer.
metadata.name: redisNom unique du Pod dans le namespace. C'est l'identifiant pour kubectl get pod redis.
metadata.labelsPaires clé-valeur pour identifier et sélectionner le Pod. Essentiels pour les Services.
labels.app: redisLabel applicatif. Convention commune pour identifier l'application.
labels.tier: backendLabel de couche. Indique que c'est un composant backend (vs frontend).
spec.containersListe des containers du Pod. Un Pod peut en avoir plusieurs (sidecar pattern).
containers[0].nameNom du container. Utile pour kubectl logs redis -c redis.
containers[0].imageImage Docker à utiliser. redis = docker.io/library/redis:latest.
containers[0].portsPorts exposés par le container. Informatif, pas de filtrage.
containerPort: 6379Port standard de Redis. Doit correspondre à ce qu'écoute l'application.

Déployer et tester

sh
# Créer le Pod
kubectl apply -f k8s-resources/redis-pod.yaml

# Vérifier l'état
kubectl get pods
# NAME    READY   STATUS    RESTARTS   AGE
# redis   1/1     Running   0          30s

# Voir les détails
kubectl describe pod redis

# Tester Redis directement
kubectl exec -it redis -- redis-cli PING
# PONG

NOTE

Pourquoi les labels sont importants ? Les labels app: redis et tier: backend seront utilisés par le Service pour trouver ce Pod. Sans labels, le Service ne pourrait pas router le trafic.


1.2 Le Pod Guestbook (frontend)

yaml
# k8s-resources/guestbook-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: guestbook
  labels:
    app: guestbook
    tier: frontend
spec:
  containers:
  - name: guestbook
    image: omerxx/guestbook-example
    imagePullPolicy: Always
    ports:
    - name: http-server
      containerPort: 3000

Nouveautés par rapport au Pod Redis

LigneExplication
imagePullPolicy: AlwaysForce Kubernetes à toujours télécharger l'image, même si elle existe localement. Utile en développement pour avoir la dernière version. Autres valeurs : IfNotPresent (défaut), Never.
ports[0].name: http-serverNom du port. Permet de le référencer par nom dans le Service au lieu du numéro. Plus lisible et maintenable.

Comment l'application trouve Redis ?

Dans le code Go (main.go), on voit :

go
redisURL := os.Getenv("REDIS_URL")
if redisURL == "" {
    redisURL = "localhost:6379"
}

L'application cherche la variable d'environnement REDIS_URL. Si elle n'existe pas, elle tente localhost:6379.

Problème : dans Kubernetes, chaque Pod a sa propre IP. localhost ne fonctionnera pas !

Solution : on va créer un Service qui expose Redis avec un nom DNS prévisible.


Partie 2 : Les Services

Les Services résolvent deux problèmes :

  1. Découverte : donner un nom DNS stable aux Pods
  2. Load balancing : répartir le trafic entre plusieurs réplicas

2.1 Service Redis (ClusterIP)

yaml
# k8s-resources/redis-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis
  labels:
    app: redis
    tier: backend
spec:
  ports:
  - port: 6379
    targetPort: 6379
  selector:
    app: redis
    tier: backend

Analyse ligne par ligne

LigneExplication
kind: ServiceObjet Service Kubernetes.
metadata.name: redisCrucial ! Ce nom devient le hostname DNS : redis.default.svc.cluster.local (ou simplement redis dans le même namespace).
spec.ports[0].portPort exposé par le Service. Les clients se connectent à redis:6379.
spec.ports[0].targetPortPort du container vers lequel le trafic est envoyé. Doit correspondre à containerPort du Pod.
spec.selectorLe cœur du Service ! Sélectionne les Pods avec ces labels. Le trafic sera routé uniquement vers les Pods qui matchent.

Type de Service

Aucun type n'est spécifié, donc c'est le type par défaut : ClusterIP.

  • Crée une IP virtuelle interne au cluster
  • Accessible uniquement depuis l'intérieur du cluster
  • Parfait pour les backends (bases de données, caches, etc.)

Comment fonctionne le selector ?

Service redis                     Pods du cluster
┌─────────────────┐              ┌─────────────────┐
│ selector:       │              │ Pod: redis      │
│   app: redis    │─────────────▶│ labels:         │ ✓ Match !
│   tier: backend │              │   app: redis    │
└─────────────────┘              │   tier: backend │
                                 └─────────────────┘
                                 
                                 ┌─────────────────┐
                                 │ Pod: guestbook  │
                                 │ labels:         │ ✗ Pas de match
                                 │   app: guestbook│
                                 │   tier: frontend│
                                 └─────────────────┘

Déployer et vérifier

sh
kubectl apply -f k8s-resources/redis-service.yaml

# Voir le Service
kubectl get svc redis
# NAME    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
# redis   ClusterIP   10.96.xxx.xxx   <none>        6379/TCP   10s

# Vérifier les endpoints (Pods sélectionnés)
kubectl get endpoints redis
# NAME    ENDPOINTS           AGE
# redis   10.244.0.5:6379     10s

# L'IP dans ENDPOINTS est celle du Pod redis
kubectl get pod redis -o wide
# NAME    READY   STATUS    IP
# redis   1/1     Running   10.244.0.5

2.2 Service Frontend (LoadBalancer)

yaml
# k8s-resources/guestbook-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: frontend
  labels:
    app: guestbook
    tier: frontend
spec:
  ports:
  - port: 3000
    targetPort: http-server
  selector:
    app: guestbook
  type: LoadBalancer

Différences avec le Service Redis

LigneExplication
targetPort: http-serverAu lieu d'un numéro, on utilise le nom du port défini dans le Pod (ports[0].name: http-server). Plus maintenable : si le port change, on ne modifie qu'un seul endroit.
selector.app: guestbookSélectionne tous les Pods avec app: guestbook, peu importe leur tier.
type: LoadBalancerExpose le Service à l'extérieur du cluster via un load balancer. Sur le cloud (AWS, GCP), provisionne un vrai LB. Sur Minikube, utilisez minikube tunnel.

Déployer et accéder

sh
kubectl apply -f k8s-resources/guestbook-service.yaml

# Voir le Service
kubectl get svc frontend
# NAME       TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
# frontend   LoadBalancer   10.96.xxx.xxx   <pending>     3000:32xxx/TCP   10s

# Sur Minikube, ouvrir un tunnel dans un autre terminal
minikube tunnel

# L'EXTERNAL-IP devrait maintenant être assignée
kubectl get svc frontend
# NAME       TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
# frontend   LoadBalancer   10.96.xxx.xxx   127.0.0.1       3000:32xxx/TCP   1m

Ouvrez http://127.0.0.1:3000 (ou l'IP externe) dans votre navigateur.


Partie 3 : Les Deployments

Les Pods seuls sont fragiles : si un Pod meurt, il n'est pas recréé. Les Deployments résolvent ce problème.

3.1 Deployment Redis

yaml
# k8s-resources/redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: redis
spec:
  selector:
    matchLabels:
      app: redis
      tier: backend
  replicas: 1
  template:
    metadata:
      labels:
        app: redis
        role: master
        tier: backend
    spec:
      containers:
      - name: master
        image: redis
        ports:
        - containerPort: 6379

Analyse ligne par ligne

LigneExplication
apiVersion: apps/v1Les Deployments sont dans le groupe API apps, pas v1 core.
kind: DeploymentObjet qui gère le cycle de vie des Pods.
spec.selector.matchLabelsObligatoire ! Le Deployment doit savoir quels Pods il gère. Doit matcher les labels du template.
spec.replicas: 1Nombre de Pods souhaités. Le Deployment maintiendra toujours ce nombre.
spec.templateTemplate du Pod à créer. C'est une définition de Pod complète (sans apiVersion/kind).
template.metadata.labelsLabels appliqués aux Pods créés. Doivent inclure ceux du selector.matchLabels.
labels.role: masterLabel supplémentaire. Utile pour distinguer master/replica dans un setup Redis avancé.

Différence avec un Pod simple

Pod simple                          Deployment
┌─────────────────┐                ┌─────────────────┐
│ Si le Pod       │                │ Controller      │
│ crash...        │                │ Manager         │
│                 │                │      │          │
│    ┌─────┐      │                │      ▼          │
│    │ Pod │ ──X  │                │ ┌─────────┐     │
│    └─────┘      │                │ │Deployment│    │
│                 │                │ └────┬────┘     │
│ ...il reste     │                │      │ observe  │
│ mort.           │                │      ▼          │
│                 │                │ replicas: 1     │
│                 │                │ actual: 0       │
│                 │                │      │          │
│                 │                │      ▼ crée     │
│                 │                │  ┌─────┐        │
│                 │                │  │ Pod │        │
│                 │                │  └─────┘        │
└─────────────────┘                └─────────────────┘

Déployer

sh
# Supprimer le Pod simple d'abord (si créé précédemment)
kubectl delete pod redis --ignore-not-found

# Créer le Deployment
kubectl apply -f k8s-resources/redis-deployment.yaml

# Voir le Deployment
kubectl get deployment redis
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# redis   1/1     1            1           30s

# Voir le Pod créé automatiquement
kubectl get pods -l app=redis
# NAME                     READY   STATUS    RESTARTS   AGE
# redis-7d8b4c5f4c-xj2km   1/1     Running   0          30s

NOTE

Remarquez le nom du Pod : redis-7d8b4c5f4c-xj2km

  • redis : nom du Deployment
  • 7d8b4c5f4c : hash du ReplicaSet (généré automatiquement)
  • xj2km : identifiant unique du Pod

3.2 Deployment Guestbook

yaml
# k8s-resources/guestbook-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: guestbook
  labels:
    app: guestbook
spec:
  selector:
    matchLabels:
      app: guestbook
      tier: frontend
  replicas: 3
  template:
    metadata:
      labels:
        app: guestbook
        tier: frontend
    spec:
      containers:
      - name: guestbook
        image: omerxx/guestbook-example
        imagePullPolicy: Always
        ports:
        - name: http-server
          containerPort: 3000

Nouveautés

LigneExplication
replicas: 33 instances du frontend pour la haute disponibilité et le load balancing.
metadata.labels (sur le Deployment)Labels sur le Deployment lui-même (pas les Pods). Utile pour kubectl get deployment -l app=guestbook.

Déployer et observer le scaling

sh
# Supprimer le Pod simple d'abord
kubectl delete pod guestbook --ignore-not-found

# Créer le Deployment
kubectl apply -f k8s-resources/guestbook-deployment.yaml

# Voir les 3 réplicas
kubectl get pods -l app=guestbook
# NAME                         READY   STATUS    RESTARTS   AGE
# guestbook-5f4d7b8c9-abc12    1/1     Running   0          30s
# guestbook-5f4d7b8c9-def34    1/1     Running   0          30s
# guestbook-5f4d7b8c9-ghi56    1/1     Running   0          30s

# Le Service frontend load-balance entre les 3
kubectl get endpoints frontend
# NAME       ENDPOINTS                                         AGE
# frontend   10.244.0.6:3000,10.244.0.7:3000,10.244.0.8:3000   5m

Tester le self-healing

sh
# Supprimer un Pod manuellement
kubectl delete pod guestbook-5f4d7b8c9-abc12

# Observer : un nouveau Pod est immédiatement créé !
kubectl get pods -l app=guestbook -w
# NAME                         READY   STATUS              RESTARTS   AGE
# guestbook-5f4d7b8c9-def34    1/1     Running             0          2m
# guestbook-5f4d7b8c9-ghi56    1/1     Running             0          2m
# guestbook-5f4d7b8c9-xyz99    0/1     ContainerCreating   0          2s
# guestbook-5f4d7b8c9-xyz99    1/1     Running             0          5s

Partie 4 : ConfigMaps et Secrets

4.1 ConfigMap

yaml
# k8s-resources/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: example-configmap
data:
  my-config-key: "This is the config value"

À quoi ça sert ?

Les ConfigMaps permettent d'externaliser la configuration de vos applications :

  • Variables d'environnement
  • Fichiers de configuration
  • Arguments de commande

Avantage : changer la config sans reconstruire l'image Docker.

Déployer et inspecter

sh
kubectl apply -f k8s-resources/configmap.yaml

# Voir le contenu
kubectl get configmap example-configmap -o yaml
kubectl describe configmap example-configmap

4.2 Pod utilisant un ConfigMap

yaml
# k8s-resources/pod-using-cm.yaml
apiVersion: v1
kind: Pod
metadata:
  name: pod-using-configmap
spec:
  containers:
  - name: example-container
    image: busybox:latest
    command: ["sh", "-c", "echo $(MY_CONFIG_KEY) && sleep 3600"]
    env:
    - name: MY_CONFIG_KEY
      valueFrom:
        configMapKeyRef:
          name: example-configmap
          key: my-config-key
  restartPolicy: Never

Analyse

LigneExplication
command: [...]Remplace le CMD du Dockerfile. Ici, affiche la variable puis dort.
env[0].nameNom de la variable d'environnement dans le container.
valueFrom.configMapKeyRefInjecte une valeur depuis un ConfigMap.
configMapKeyRef.nameNom du ConfigMap à utiliser.
configMapKeyRef.keyClé dans le ConfigMap dont on veut la valeur.
restartPolicy: NeverNe pas redémarrer le Pod s'il s'arrête. Utile pour les jobs.

Tester

sh
kubectl apply -f k8s-resources/pod-using-cm.yaml

# Voir les logs (la valeur de la config)
kubectl logs pod-using-configmap
# This is the config value

# Vérifier la variable dans le container
kubectl exec pod-using-configmap -- env | grep MY_CONFIG
# MY_CONFIG_KEY=This is the config value

4.3 Secret

yaml
# k8s-resources/secret-configmap.yaml
apiVersion: v1
kind: Secret
metadata:
  name: example-secret
type: Opaque
data:
  my-secret-key: c2VjcmV0LXZhbHVl  # base64 encoded value

Analyse

LigneExplication
kind: SecretObjet pour stocker des données sensibles.
type: OpaqueType générique (vs kubernetes.io/tls pour les certificats, etc.).
data.my-secret-keyValeur encodée en base64.

Encoder/Décoder en base64

sh
# Encoder
echo -n "secret-value" | base64
# c2VjcmV0LXZhbHVl

# Décoder
echo "c2VjcmV0LXZhbHVl" | base64 -d
# secret-value

WARNING

Base64 n'est PAS du chiffrement ! Les Secrets sont encodés, pas chiffrés. Toute personne ayant accès au cluster peut les lire. En production, utilisez des solutions comme Vault ou Sealed Secrets.


Partie 5 : Jobs et CronJobs

5.1 Job (tâche ponctuelle)

yaml
# k8s-resources/job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: example-job
spec:
  template:
    spec:
      containers:
      - name: example
        image: busybox:latest
        command: ["sh", "-c", "echo Hello, Kubernetes! && sleep 10"]
      restartPolicy: Never
  backoffLimit: 4

Analyse

LigneExplication
apiVersion: batch/v1Les Jobs sont dans le groupe API batch.
kind: JobTâche qui s'exécute jusqu'à complétion (vs Deployment qui tourne en continu).
spec.templateTemplate du Pod à créer (comme un Deployment).
restartPolicy: NeverObligatoire pour les Jobs. Un Job qui échoue ne doit pas redémarrer en boucle.
backoffLimit: 4Nombre de tentatives avant d'abandonner. Après 4 échecs, le Job est marqué Failed.

Cas d'usage

  • Migrations de base de données
  • Traitements batch (export de données, génération de rapports)
  • Tâches d'initialisation

Déployer et observer

sh
kubectl apply -f k8s-resources/job.yaml

# Suivre l'exécution
kubectl get jobs -w
# NAME          COMPLETIONS   DURATION   AGE
# example-job   0/1           5s         5s
# example-job   1/1           12s        12s

# Voir les logs
kubectl logs job/example-job
# Hello, Kubernetes!

# Le Pod reste visible après complétion (statut Completed)
kubectl get pods -l job-name=example-job
# NAME                READY   STATUS      RESTARTS   AGE
# example-job-abc12   0/1     Completed   0          1m

5.2 CronJob (tâche planifiée)

yaml
# k8s-resources/cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: example-cronjob
spec:
  schedule: "*/5 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: example
            image: busybox:latest
            command: ["sh", "-c", "echo Hello, Kubernetes! && sleep 10"]
          restartPolicy: Never
      backoffLimit: 4

Analyse

LigneExplication
kind: CronJobJob planifié selon un schedule cron.
spec.scheduleExpression cron : */5 * * * * = toutes les 5 minutes.
spec.jobTemplateTemplate du Job à créer à chaque exécution.

Format cron

┌───────────── minute (0 - 59)
│ ┌───────────── heure (0 - 23)
│ │ ┌───────────── jour du mois (1 - 31)
│ │ │ ┌───────────── mois (1 - 12)
│ │ │ │ ┌───────────── jour de la semaine (0 - 6, dimanche = 0)
│ │ │ │ │
* * * * *

Exemples :

  • 0 * * * * : toutes les heures (à :00)
  • 0 0 * * * : tous les jours à minuit
  • 0 0 * * 0 : tous les dimanches à minuit
  • */5 * * * * : toutes les 5 minutes

Déployer et observer

sh
kubectl apply -f k8s-resources/cronjob.yaml

# Voir le CronJob
kubectl get cronjobs
# NAME              SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
# example-cronjob   */5 * * * *   False     0        <none>          10s

# Attendre 5 minutes et voir les Jobs créés
kubectl get jobs -l job-name=example-cronjob

Partie 6 : DaemonSet

yaml
# k8s-resources/daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nginx-daemonset
  labels:
    app: nginx
spec:
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80

Différence avec un Deployment

DeploymentDaemonSet
replicas: N pods, répartis selon le scheduler1 pod par nœud (automatiquement)
Pour les applicationsPour les agents système (logs, monitoring, réseau)
Peut avoir 0 pod sur certains nœudsGarantit 1 pod sur chaque nœud

Cas d'usage

  • Collecteurs de logs : Fluentd, Filebeat
  • Agents de monitoring : Node Exporter, Datadog agent
  • Plugins réseau : Calico, Cilium
  • Stockage : Longhorn, Ceph agents

Déployer et observer

sh
kubectl apply -f k8s-resources/daemonset.yaml

# Voir le DaemonSet
kubectl get daemonset
# NAME              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR   AGE
# nginx-daemonset   1         1         1       1            1           <none>          30s

# Un Pod par nœud
kubectl get pods -l app=nginx -o wide
# NAME                    READY   STATUS    NODE
# nginx-daemonset-abc12   1/1     Running   minikube

# Si vous ajoutez un nœud, un Pod sera automatiquement créé dessus
minikube node add
kubectl get pods -l app=nginx -o wide
# NAME                    READY   STATUS    NODE
# nginx-daemonset-abc12   1/1     Running   minikube
# nginx-daemonset-def34   1/1     Running   minikube-m02

Partie 7 : StatefulSet

yaml
# k8s-resources/statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: web
spec:
  serviceName: "nginx"
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:latest
        ports:
        - containerPort: 80
          name: web
        volumeMounts:
        - name: www
          mountPath: /usr/share/nginx/html
  volumeClaimTemplates:
  - metadata:
      name: www
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

Différences avec un Deployment

DeploymentStatefulSet
Noms de pods aléatoires (app-7d8b4-xj2km)Noms prévisibles (web-0, web-1, web-2)
Pods interchangeablesIdentité stable
Stockage partagé ou éphémèrePVC dédié par pod
Ordre de déploiement quelconqueOrdre garanti (0, puis 1, puis 2)

Analyse des nouveautés

LigneExplication
spec.serviceNameNom du Service Headless associé. Requis pour le DNS des pods individuels.
volumeClaimTemplatesTemplate de PVC. Un PVC unique sera créé pour chaque pod (www-web-0, www-web-1).
accessModes: ["ReadWriteOnce"]Le volume peut être monté en lecture-écriture par un seul nœud.
resources.requests.storageTaille du volume demandée.

Cas d'usage

  • Bases de données (PostgreSQL, MySQL, MongoDB)
  • Systèmes distribués (Kafka, Elasticsearch, Zookeeper)
  • Tout ce qui a besoin d'une identité stable et de données persistantes

Déployer et observer

sh
kubectl apply -f k8s-resources/statefulset.yaml

# Voir le StatefulSet
kubectl get statefulset
# NAME   READY   AGE
# web    1/1     30s

# Nom de pod prévisible !
kubectl get pods -l app=nginx
# NAME    READY   STATUS    RESTARTS   AGE
# web-0   1/1     Running   0          30s

# PVC créé automatiquement
kubectl get pvc
# NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# www-web-0   Bound    pvc-abc123-def456-ghi789                   1Gi        RWO            standard       30s

Partie 8 : Affectation des Pods aux nœuds

8.1 nodeName (affectation directe)

yaml
# k8s-resources/pod-with-nodename.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  nodeName: minikube

Analyse

LigneExplication
nodeName: minikubeForce le pod sur ce nœud spécifique. Bypass le scheduler.

WARNING

À éviter en production !nodeName lie le pod à un nœud spécifique. Si le nœud tombe, le pod ne sera jamais replanifié ailleurs. Utilisez plutôt nodeSelector ou les affinités.


8.2 nodeSelector

yaml
# k8s-resources/pod-with-node-selector.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  nodeSelector:
    disktype: ssd
  containers:
  - name: nginx
    image: nginx

Analyse

LigneExplication
nodeSelector.disktype: ssdLe pod sera schedulé uniquement sur les nœuds ayant le label disktype=ssd.

Utilisation

sh
# D'abord, labelliser un nœud
kubectl label nodes minikube disktype=ssd

# Puis déployer le pod
kubectl apply -f k8s-resources/pod-with-node-selector.yaml

# Vérifier
kubectl get pod nginx -o wide
# NAME    READY   STATUS    NODE
# nginx   1/1     Running   minikube

8.3 Tolerations (pour les taints)

yaml
# k8s-resources/pod-with-tolerations.yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx
  tolerations:
  - key: "test-environment"
    operator: "Exists"
    effect: "NoSchedule"

Rappel : Taints et Tolerations

  • Taint (sur le nœud) : repousse les pods
  • Toleration (sur le pod) : permet au pod d'ignorer une taint

Analyse

LigneExplication
tolerations[0].keyClé de la taint à tolérer.
operator: "Exists"La taint existe, peu importe sa valeur. Alternative : Equal pour matcher une valeur précise.
effect: "NoSchedule"Type de taint tolérée. Doit correspondre à l'effet de la taint sur le nœud.

Utilisation

sh
# Ajouter une taint à un nœud
kubectl taint nodes minikube test-environment=true:NoSchedule

# Sans toleration, les nouveaux pods ne seront pas schedulés sur ce nœud
kubectl run test --image=nginx
kubectl get pod test
# NAME   READY   STATUS    RESTARTS   AGE
# test   0/1     Pending   0          30s  # Reste Pending !

# Avec toleration, le pod peut y aller
kubectl apply -f k8s-resources/pod-with-tolerations.yaml
kubectl get pod nginx -o wide
# NAME    READY   STATUS    NODE
# nginx   1/1     Running   minikube

# Nettoyer
kubectl taint nodes minikube test-environment:NoSchedule-

Partie 9 : Déploiement complet

Maintenant que vous comprenez chaque composant, déployons l'application complète.

Ordre de déploiement recommandé

sh
# 1. ConfigMaps et Secrets (si nécessaire)
kubectl apply -f k8s-resources/configmap.yaml

# 2. Backend (Redis)
kubectl apply -f k8s-resources/redis-deployment.yaml
kubectl apply -f k8s-resources/redis-service.yaml

# 3. Attendre que Redis soit prêt
kubectl wait --for=condition=ready pod -l app=redis --timeout=60s

# 4. Frontend (Guestbook)
kubectl apply -f k8s-resources/guestbook-deployment.yaml
kubectl apply -f k8s-resources/guestbook-service.yaml

# 5. Vérifier
kubectl get all -l app=guestbook
kubectl get all -l app=redis

Accéder à l'application

sh
# Sur Minikube
minikube tunnel  # Dans un autre terminal

# Ou utiliser le port-forward
kubectl port-forward svc/frontend 3000:3000

# Ouvrir http://localhost:3000

Partie 10 : Cycle de vie et comportements Kubernetes

Cette partie est cruciale pour comprendre la puissance de Kubernetes. Nous allons observer en temps réel comment le cluster réagit à différentes situations.

10.1 Self-Healing : résurrection automatique des Pods

Le self-healing est la capacité de Kubernetes à maintenir l'état désiré automatiquement.

Concept : La boucle de réconciliation

┌─────────────────────────────────────────────────────────────┐
│                    Controller Manager                        │
│                                                              │
│   ┌─────────────┐         ┌─────────────┐                   │
│   │ État désiré │         │ État actuel │                   │
│   │ replicas: 3 │    ≠    │ replicas: 2 │                   │
│   └─────────────┘         └─────────────┘                   │
│          │                       │                          │
│          └───────────┬───────────┘                          │
│                      ▼                                       │
│              ┌───────────────┐                              │
│              │  Réconcilier  │                              │
│              │ Créer 1 pod   │                              │
│              └───────────────┘                              │
└─────────────────────────────────────────────────────────────┘

Le Controller Manager compare en permanence l'état désiré (défini dans le Deployment) avec l'état actuel du cluster. S'il y a une différence, il agit pour corriger.

TP : Tuer un Pod et observer la résurrection

sh
# Vérifier l'état initial (3 pods)
kubectl get pods -l app=guestbook
# NAME                         READY   STATUS    RESTARTS   AGE
# guestbook-5f4d7b8c9-abc12    1/1     Running   0          5m
# guestbook-5f4d7b8c9-def34    1/1     Running   0          5m
# guestbook-5f4d7b8c9-ghi56    1/1     Running   0          5m

# Ouvrir un second terminal pour observer en temps réel
kubectl get pods -l app=guestbook -w

# Dans le premier terminal, supprimer un pod
kubectl delete pod guestbook-5f4d7b8c9-abc12

# Observer dans le second terminal :
# guestbook-5f4d7b8c9-abc12    1/1     Terminating   0          5m
# guestbook-5f4d7b8c9-xyz99    0/1     Pending       0          0s
# guestbook-5f4d7b8c9-xyz99    0/1     ContainerCreating   0   0s
# guestbook-5f4d7b8c9-xyz99    1/1     Running       0          2s

Ce qui s'est passé :

  1. Vous avez supprimé un pod → état actuel = 2 pods
  2. Le Controller Manager détecte : 2 ≠ 3 (désiré)
  3. Il crée immédiatement un nouveau pod
  4. En ~2 secondes, le cluster est revenu à l'état désiré

TP : Tuer TOUS les Pods en même temps

sh
# Supprimer tous les pods guestbook d'un coup
kubectl delete pods -l app=guestbook --wait=false

# Observer : TOUS sont recréés immédiatement
kubectl get pods -l app=guestbook -w
# Tous en Terminating, puis 3 nouveaux en Creating, puis Running

Point clé : Même si vous supprimez tous les pods, Kubernetes les recrée. C'est la magie du Deployment.

TP : Simuler un crash d'application

sh
# Entrer dans un pod et tuer le processus principal
kubectl exec -it $(kubectl get pod -l app=guestbook -o jsonpath='{.items[0].metadata.name}') -- /bin/sh

# Dans le container, trouver et tuer le processus (PID 1)
kill 1
# Le container s'arrête, vous êtes éjecté

# Observer : le pod redémarre (RESTARTS augmente)
kubectl get pods -l app=guestbook
# NAME                         READY   STATUS    RESTARTS   AGE
# guestbook-5f4d7b8c9-xyz99    1/1     Running   1          10m  # RESTARTS = 1

Différence importante :

  • Si le container crash → le Pod redémarre le container (RESTARTS augmente)
  • Si le Pod est supprimé → le Deployment crée un nouveau Pod (nouveau nom)

10.2 Scaling : ajuster le nombre de réplicas

Scaling manuel

sh
# État initial : 3 réplicas
kubectl get deployment guestbook
# NAME        READY   UP-TO-DATE   AVAILABLE   AGE
# guestbook   3/3     3            3           15m

# Scale UP à 5 réplicas
kubectl scale deployment guestbook --replicas=5

# Observer la création des nouveaux pods
kubectl get pods -l app=guestbook -w
# 2 nouveaux pods apparaissent en Creating puis Running

# Vérifier les endpoints (le Service les découvre automatiquement)
kubectl get endpoints frontend
# NAME       ENDPOINTS
# frontend   10.244.0.6:3000,10.244.0.7:3000,10.244.0.8:3000,10.244.0.9:3000,10.244.0.10:3000

# Scale DOWN à 2 réplicas
kubectl scale deployment guestbook --replicas=2

# Observer : 3 pods sont terminés
kubectl get pods -l app=guestbook -w
# 3 pods passent en Terminating puis disparaissent

Observer le load balancing

sh
# Identifier quel pod répond (l'app affiche son hostname)
# Ouvrez http://localhost:3000 et notez l'URL affichée

# Rafraîchissez plusieurs fois : l'URL change car le Service
# load-balance entre les différents pods

# Vous pouvez aussi tester avec curl en boucle
for i in {1..10}; do
  curl -s http://localhost:3000/env | grep HOSTNAME
done
# Vous verrez différents noms de pods

Scaling à zéro (arrêt temporaire)

sh
# Mettre l'application en pause sans la supprimer
kubectl scale deployment guestbook --replicas=0

# Vérifier : plus aucun pod
kubectl get pods -l app=guestbook
# No resources found

# Le Deployment existe toujours !
kubectl get deployment guestbook
# NAME        READY   UP-TO-DATE   AVAILABLE   AGE
# guestbook   0/0     0            0           20m

# Redémarrer
kubectl scale deployment guestbook --replicas=3

10.3 Rolling Update : mise à jour sans interruption

Le Rolling Update permet de mettre à jour une application sans downtime.

Concept : mise à jour progressive

État initial (v1)           Pendant le rolling update       État final (v2)
┌─────┐ ┌─────┐ ┌─────┐    ┌─────┐ ┌─────┐ ┌─────┐        ┌─────┐ ┌─────┐ ┌─────┐
│ v1  │ │ v1  │ │ v1  │    │ v1  │ │ v1  │ │ v2  │        │ v2  │ │ v2  │ │ v2  │
└─────┘ └─────┘ └─────┘    └─────┘ └─────┘ └─────┘        └─────┘ └─────┘ └─────┘


                           1. Créer un pod v2
                           2. Attendre qu'il soit Ready
                           3. Supprimer un pod v1
                           4. Répéter jusqu'à 100% v2

TP : Observer un Rolling Update

sh
# Terminal 1 : Observer les pods en temps réel
kubectl get pods -l app=guestbook -w

# Terminal 2 : Observer le rollout
kubectl rollout status deployment/guestbook -w

# Terminal 3 : Déclencher la mise à jour (changer l'image)
kubectl set image deployment/guestbook guestbook=omerxx/guestbook-example:latest

# Observer dans Terminal 1 :
# - De nouveaux pods apparaissent (avec un nouveau ReplicaSet hash)
# - Les anciens pods sont progressivement terminés
# - À tout moment, des pods sont disponibles (pas de downtime)

TP : Mise à jour avec suivi détaillé

sh
# Voir l'historique des révisions
kubectl rollout history deployment/guestbook
# REVISION  CHANGE-CAUSE
# 1         <none>
# 2         <none>

# Voir les détails d'une révision
kubectl rollout history deployment/guestbook --revision=2

# Voir les ReplicaSets (un par révision)
kubectl get replicasets -l app=guestbook
# NAME                   DESIRED   CURRENT   READY   AGE
# guestbook-5f4d7b8c9    0         0         0       30m   # ancienne version
# guestbook-7a8b9c0d1    3         3         3       2m    # nouvelle version

Paramètres du Rolling Update

Dans le Deployment, vous pouvez configurer la stratégie :

yaml
spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # Combien de pods EN PLUS peuvent être créés
      maxUnavailable: 0  # Combien de pods peuvent être indisponibles
ParamètreValeurEffet
maxSurge: 1, maxUnavailable: 0PrudentCrée d'abord, supprime ensuite. Jamais moins de 3 pods.
maxSurge: 0, maxUnavailable: 1ÉconomeSupprime d'abord, crée ensuite. Utilise moins de ressources.
maxSurge: 2, maxUnavailable: 2RapideMet à jour 2 pods à la fois. Plus rapide mais plus risqué.

10.4 Rollback : revenir en arrière

Si une mise à jour pose problème, Kubernetes permet de revenir instantanément à la version précédente.

TP : Simuler un déploiement raté et rollback

sh
# Déployer une "mauvaise" version (image qui n'existe pas)
kubectl set image deployment/guestbook guestbook=omerxx/guestbook-example:version-inexistante

# Observer : les nouveaux pods sont en erreur
kubectl get pods -l app=guestbook
# NAME                         READY   STATUS             RESTARTS   AGE
# guestbook-7a8b9c0d1-abc12    1/1     Running            0          5m   # ancien, OK
# guestbook-7a8b9c0d1-def34    1/1     Running            0          5m   # ancien, OK
# guestbook-9z8y7x6w5-xyz99    0/1     ImagePullBackOff   0          30s  # nouveau, ERREUR

# Le rollout est bloqué
kubectl rollout status deployment/guestbook
# Waiting for deployment "guestbook" rollout to finish: 1 out of 3 new replicas have been updated...

# ROLLBACK !
kubectl rollout undo deployment/guestbook

# Observer : retour à la normale
kubectl get pods -l app=guestbook
# Tous les pods sont Running avec l'ancienne image

# Vérifier l'historique
kubectl rollout history deployment/guestbook
# REVISION  CHANGE-CAUSE
# 2         <none>
# 3         <none>  # Le rollback crée une nouvelle révision

Rollback vers une révision spécifique

sh
# Voir toutes les révisions
kubectl rollout history deployment/guestbook

# Rollback vers la révision 1
kubectl rollout undo deployment/guestbook --to-revision=1

10.5 Liveness et Readiness Probes en action

Les probes permettent à Kubernetes de savoir si un container est sain.

Concept

ProbeQuestionAction si échec
Liveness"Es-tu vivant ?"Redémarrer le container
Readiness"Es-tu prêt à recevoir du trafic ?"Retirer du Service (plus de trafic)

TP : Ajouter des probes au Deployment

Créons une version améliorée du Deployment avec des probes :

sh
# Éditer le deployment
kubectl edit deployment guestbook

Ajoutez dans spec.template.spec.containers[0] :

yaml
        livenessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 10
          failureThreshold: 3
        readinessProbe:
          httpGet:
            path: /
            port: 3000
          initialDelaySeconds: 2
          periodSeconds: 5
          failureThreshold: 3

Observer les probes en action

sh
# Voir les probes dans la description du pod
kubectl describe pod -l app=guestbook | grep -A5 "Liveness\|Readiness"

# Simuler un pod "pas prêt" en tuant Redis
kubectl delete pod -l app=redis

# Les pods guestbook échouent leur probe (ne peuvent plus joindre Redis)
# Ils sont retirés des endpoints du Service
kubectl get endpoints frontend
# Les endpoints disparaissent progressivement

# Recréer Redis
kubectl apply -f k8s-resources/redis-deployment.yaml

# Les probes réussissent à nouveau, les pods reviennent dans les endpoints
kubectl get endpoints frontend

10.6 Resource Limits et comportement sous pression

TP : Observer le throttling CPU

sh
# Créer un pod avec une limite CPU très basse
kubectl run stress-test --image=polinux/stress --restart=Never \
  --limits="cpu=100m,memory=128Mi" \
  -- stress --cpu 2 --timeout 60s

# Observer la consommation (le pod est throttlé à 100m)
kubectl top pod stress-test
# NAME          CPU(cores)   MEMORY(bytes)
# stress-test   100m         5Mi            # Bloqué à 100m malgré stress --cpu 2

# Le pod n'est PAS tué, juste ralenti
kubectl get pod stress-test
# STATUS: Running

TP : Observer un OOMKill

sh
# Créer un pod qui va dépasser sa limite mémoire
kubectl run oom-test --image=polinux/stress --restart=Never \
  --limits="memory=64Mi" \
  -- stress --vm 1 --vm-bytes 128M --timeout 60s

# Observer : le pod est tué (OOMKilled)
kubectl get pod oom-test -w
# NAME       READY   STATUS      RESTARTS   AGE
# oom-test   0/1     OOMKilled   0          5s

# Voir la raison
kubectl describe pod oom-test | grep -A3 "Last State"
# Last State:     Terminated
#   Reason:       OOMKilled

Résumé :

  • CPU : throttling (ralenti mais pas tué)
  • Mémoire : OOMKill (tué et redémarré)

10.7 Service Discovery et DNS

Comment les pods se trouvent-ils ?

Kubernetes fournit un DNS interne. Chaque Service a une entrée DNS.

sh
# Créer un pod de debug
kubectl run debug --image=busybox --restart=Never -- sleep 3600

# Résoudre le nom DNS du service Redis
kubectl exec debug -- nslookup redis
# Server:    10.96.0.10
# Address 1: 10.96.0.10 kube-dns.kube-system.svc.cluster.local
#
# Name:      redis
# Address 1: 10.96.xxx.xxx redis.default.svc.cluster.local

# Différents formats DNS possibles
kubectl exec debug -- nslookup redis                    # court
kubectl exec debug -- nslookup redis.default            # avec namespace
kubectl exec debug -- nslookup redis.default.svc        # avec svc
kubectl exec debug -- nslookup redis.default.svc.cluster.local  # FQDN

Voir les variables d'environnement injectées

sh
# Kubernetes injecte des variables pour chaque Service
kubectl exec debug -- env | grep -i redis
# REDIS_SERVICE_HOST=10.96.xxx.xxx
# REDIS_SERVICE_PORT=6379
# REDIS_PORT=tcp://10.96.xxx.xxx:6379

10.8 Que se passe-t-il quand un nœud tombe ?

Sur un cluster multi-nœuds, Kubernetes gère la panne d'un nœud.

TP : Simuler une panne de nœud (Minikube multi-nodes)

sh
# Ajouter un second nœud
minikube node add

# Vérifier les nœuds
kubectl get nodes
# NAME           STATUS   ROLES           AGE   VERSION
# minikube       Ready    control-plane   1h    v1.28.0
# minikube-m02   Ready    <none>          1m    v1.28.0

# Voir la répartition des pods
kubectl get pods -o wide
# Certains pods sont sur minikube, d'autres sur minikube-m02

# Arrêter le second nœud (simuler une panne)
minikube node stop minikube-m02

# Observer : le nœud passe en NotReady après ~40s
kubectl get nodes -w
# minikube-m02   NotReady   <none>   5m    v1.28.0

# Après ~5 minutes, les pods du nœud mort sont replanifiés
kubectl get pods -o wide -w
# Les pods qui étaient sur minikube-m02 sont recréés sur minikube

Délais par défaut :

  • 40 secondes : le nœud passe en NotReady
  • 5 minutes : les pods sont considérés comme perdus et replanifiés

10.9 Événements : l'historique du cluster

Les événements sont la "boîte noire" de Kubernetes.

sh
# Voir tous les événements récents
kubectl get events --sort-by='.lastTimestamp'

# Événements d'un pod spécifique
kubectl describe pod <pod-name>
# Section "Events:" en bas

# Filtrer les événements par type
kubectl get events --field-selector type=Warning

# Événements en temps réel
kubectl get events -w

Types d'événements courants :

ÉvénementSignification
ScheduledPod assigné à un nœud
PullingTéléchargement de l'image
PulledImage téléchargée
CreatedContainer créé
StartedContainer démarré
KillingContainer en cours d'arrêt
UnhealthyProbe échouée
FailedSchedulingImpossible de trouver un nœud
BackOffRedémarrage après crash

10.10 Résumé : La philosophie "Cattle not Pets"

Tout ce que nous avons vu illustre cette philosophie :

ConceptPets (animaux de compagnie)Cattle (bétail)
IdentitéUnique, irremplaçableAnonyme, interchangeable
PanneRéparer à tout prixRemplacer
ScalingAcheter un plus gros serveurAjouter plus d'instances
Mise à jourModifier en placeRemplacer par une nouvelle version
ÉtatSur le serveurExternalisé (base de données, stockage)

Kubernetes automatise la gestion du "bétail" :

  • Un pod meurt ? → On en crée un autre
  • Besoin de plus de capacité ? → On ajoute des réplicas
  • Mise à jour ? → On remplace progressivement
  • Nœud en panne ? → On replanifie ailleurs

Récapitulatif des objets

ObjetFichierRôle
Podguestbook-pod.yaml, redis-pod.yamlUnité de base, éphémère
Deploymentguestbook-deployment.yaml, redis-deployment.yamlGère le cycle de vie des Pods
Serviceguestbook-service.yaml, redis-service.yamlExpose les Pods, DNS, load balancing
ConfigMapconfigmap.yamlConfiguration externalisée
Secretsecret-configmap.yamlDonnées sensibles
Jobjob.yamlTâche ponctuelle
CronJobcronjob.yamlTâche planifiée
DaemonSetdaemonset.yaml1 Pod par nœud
StatefulSetstatefulset.yamlApps avec état, identité stable

Nettoyage

sh
# Supprimer tout
kubectl delete -f k8s-resources/

# Ou supprimer par type
kubectl delete deployment guestbook redis
kubectl delete service frontend redis
kubectl delete configmap example-configmap
kubectl delete secret example-secret
kubectl delete job example-job
kubectl delete cronjob example-cronjob
kubectl delete daemonset nginx-daemonset
kubectl delete statefulset web
kubectl delete pvc --all