TP: Kubernetes Mise en application

Durée estimée : 5 à 7 heures Niveau : débutant à intermédiaire (notions de conteneurs et de Docker acquises) Environnement : cluster local kind (Kubernetes in Docker) sur Linux, macOS ou WSL2

Ce TP reprend l'ensemble des notions de base de Kubernetes dans l'ordre où on les utilise en pratique. Chaque partie contient des commandes à exécuter, le résultat attendu et quelques questions. Les réponses aux questions sont regroupées en annexe A.

Conseil : tapez les commandes plutôt que de les copier quand vous en avez le temps, et lisez systématiquement la sortie avant de passer à la suite.


Sommaire

  1. Mise en place de l'environnement
  2. Découverte du cluster
  3. Les Pods
  4. Labels et sélecteurs
  5. Deployments et ReplicaSets
  6. Services et DNS
  7. ConfigMaps et Secrets
  8. Probes et ressources
  9. Stockage persistant
  10. Jobs et CronJobs
  11. Helm et Ingress
  12. RBAC et ServiceAccounts
  13. Namespaces et quotas
  14. Atelier dépannage
  15. Mini-projet final
  16. Nettoyage

Annexes : A. Corrigé des questions · B. Solution du mini-projet · C. Aide-mémoire kubectl


Partie 0 : Mise en place de l'environnement

0.1 Prérequis

Docker doit être installé et fonctionnel :

docker version
docker run --rm hello-world

0.2 Installation des outils

kubectl

curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
kubectl version --client

kind (vérifiez la dernière version sur https://github.com/kubernetes-sigs/kind/releases et ajustez la variable si besoin)

KIND_VERSION=v0.30.0
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
sudo install -m 0755 kind /usr/local/bin/kind
rm kind
kind version

Helm

curl -fsSL https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
Sur macOS : brew install kubectl kind helm

0.3 Création du cluster

Créez un dossier de travail et le fichier de configuration du cluster :

mkdir -p ~/tp-k8s && cd ~/tp-k8s
cat > kind-config.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: tp-k8s
nodes:
  - role: control-plane
    extraPortMappings:
      - containerPort: 30080
        hostPort: 30080
        protocol: TCP
  - role: worker
  - role: worker
EOF

Ce cluster contient un nœud control-plane et deux nœuds workers. Le port 30080 de la machine hôte est redirigé vers le cluster, on s'en servira en partie 5.

kind create cluster --config kind-config.yaml

La création prend une à deux minutes.

0.4 Auto-complétion (recommandé)

# bash
echo 'source <(kubectl completion bash)' >> ~/.bashrc
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
source ~/.bashrc
# zsh
echo 'source <(kubectl completion zsh)' >> ~/.zshrc
echo 'alias k=kubectl' >> ~/.zshrc
source ~/.zshrc

Dans la suite du TP, on écrit kubectl en entier, mais vous pouvez utiliser k.

✅ Point de contrôle 0

kubectl get nodes

Résultat attendu : trois nœuds (tp-k8s-control-plane, tp-k8s-worker, tp-k8s-worker2) au statut Ready.


Partie 1 : Découverte du cluster

1.1 Informations générales

kubectl cluster-info
kubectl config get-contexts
kubectl config current-context

kubectl lit sa configuration dans ~/.kube/config. Un contexte associe un cluster, un utilisateur et un namespace par défaut.

1.2 Les nœuds

kubectl get nodes -o wide
kubectl describe node tp-k8s-worker

Dans la sortie de describe, repérez les sections Capacity, Allocatable, Conditions et Non-terminated Pods.

1.3 Les namespaces et les composants système

kubectl get namespaces
kubectl get pods -n kube-system -o wide

Vous devez voir notamment etcd, kube-apiserver, kube-controller-manager, kube-scheduler, kube-proxy et coredns.

1.4 Explorer l'API

kubectl api-resources | head -30
kubectl explain pod
kubectl explain pod.spec.containers
kubectl explain deployment.spec.strategy --recursive

kubectl explain est la documentation intégrée : elle donne la structure exacte attendue dans les fichiers YAML.

1.5 Créer un namespace de travail

kubectl create namespace tp
kubectl config set-context --current --namespace=tp
kubectl config view --minify | grep namespace

Toutes les commandes suivantes s'exécutent dans le namespace tp sauf indication contraire.

❓ Questions partie 1

  1. Sur quel nœud tournent etcd et kube-apiserver ? Pourquoi ?
  2. Combien de pods kube-proxy y a-t-il, et pourquoi ce nombre ?
  3. Quelle est la différence entre kubectl get et kubectl describe ?

Partie 2 : Les Pods

2.1 Créer un pod en mode impératif

kubectl run nginx --image=nginx:1.27
kubectl get pods
kubectl get pods -o wide

Attendez que le statut passe à Running. Vous pouvez suivre l'évolution en direct :

kubectl get pods -w

(Ctrl+C pour quitter.)

2.2 Inspecter le pod

kubectl describe pod nginx
kubectl logs nginx
kubectl get pod nginx -o yaml | less

Dans describe, lisez la section Events en bas : elle retrace la planification (Scheduled), le téléchargement de l'image (Pulling, Pulled) et le démarrage (Created, Started).

2.3 Entrer dans le conteneur

kubectl exec -it nginx -- bash

Dans le conteneur :

hostname
cat /etc/os-release
curl -s localhost | head -5 || echo "curl absent"
exit

Exécuter une commande unique sans shell interactif :

kubectl exec nginx -- nginx -v

2.4 Accéder au pod depuis votre poste

kubectl port-forward pod/nginx 8080:80

Dans un second terminal :

curl -s http://localhost:8080 | grep title

Arrêtez le port-forward avec Ctrl+C.

2.5 Générer un manifeste YAML

Plutôt que d'écrire le YAML de zéro, on le fait générer par kubectl :

kubectl run nginx-yaml --image=nginx:1.27 --port=80 --dry-run=client -o yaml > pod-nginx.yaml
cat pod-nginx.yaml

Nettoyez le fichier pour obtenir ceci :

apiVersion: v1
kind: Pod
metadata:
  name: nginx-yaml
  labels:
    app: nginx-yaml
spec:
  containers:
    - name: nginx
      image: nginx:1.27
      ports:
        - containerPort: 80

Appliquez-le :

kubectl apply -f pod-nginx.yaml
kubectl get pods --show-labels

2.6 Pod multi-conteneurs

Créez pod-multi.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: multi
spec:
  volumes:
    - name: partage
      emptyDir: {}
  containers:
    - name: ecrivain
      image: busybox:1.36
      command: ["sh", "-c", "while true; do date >> /data/horodatage.txt; sleep 5; done"]
      volumeMounts:
        - name: partage
          mountPath: /data
    - name: lecteur
      image: busybox:1.36
      command: ["sh", "-c", "tail -f /data/horodatage.txt"]
      volumeMounts:
        - name: partage
          mountPath: /data
kubectl apply -f pod-multi.yaml
kubectl get pod multi
kubectl logs multi -c lecteur -f

Les deux conteneurs partagent le volume emptyDir : le lecteur affiche ce qu'écrit l'écrivain.

2.7 Suppression

kubectl delete pod nginx nginx-yaml multi
kubectl get pods

❓ Questions partie 2

  1. Que signifie READY 2/2 sur le pod multi ?
  2. Que se passe-t-il si vous supprimez un pod créé avec kubectl run ? Est-il recréé ?
  3. Les deux conteneurs du pod multi ont-ils la même adresse IP ?

Partie 3 : Labels et sélecteurs

3.1 Créer des pods étiquetés

kubectl run front-prod --image=nginx:1.27 --labels="app=front,env=prod"
kubectl run front-dev  --image=nginx:1.27 --labels="app=front,env=dev"
kubectl run back-prod  --image=nginx:1.27 --labels="app=back,env=prod"
kubectl get pods --show-labels

3.2 Filtrer avec des sélecteurs

kubectl get pods -l env=prod
kubectl get pods -l app=front,env=dev
kubectl get pods -l 'env in (prod,dev)'
kubectl get pods -l 'app!=back'
kubectl get pods -L app,env

3.3 Modifier les labels

kubectl label pod front-dev env=staging --overwrite
kubectl label pod back-prod tier=backend
kubectl label pod back-prod tier-
kubectl get pods --show-labels

Le tiret final (tier-) supprime le label.

3.4 Annotations

Les annotations stockent des métadonnées non utilisées pour la sélection :

kubectl annotate pod front-prod responsable="equipe-web"
kubectl describe pod front-prod | grep -A3 Annotations

3.5 Nettoyage

kubectl delete pods -l 'app in (front,back)'

❓ Questions partie 3

  1. Quelle est la différence d'usage entre un label et une annotation ?
  2. Quelle commande supprime tous les pods ayant env=prod ?

Partie 4 : Deployments et ReplicaSets

4.1 Créer un Deployment

kubectl create deployment web --image=nginx:1.27 --replicas=3
kubectl get deployments
kubectl get replicasets
kubectl get pods -o wide

Observez la hiérarchie des noms : web → web-<hash> (ReplicaSet) → web-<hash>-<id> (Pods). Notez aussi la répartition des pods sur les deux workers.

4.2 Auto-réparation

Supprimez un pod et observez :

kubectl delete pod $(kubectl get pods -l app=web -o jsonpath='{.items[0].metadata.name}')
kubectl get pods -l app=web

Un nouveau pod est créé immédiatement pour revenir à 3 réplicas.

4.3 Mise à l'échelle

kubectl scale deployment web --replicas=6
kubectl get pods -l app=web
kubectl scale deployment web --replicas=2
kubectl get pods -l app=web

4.4 Mise à jour progressive (rolling update)

Dans un second terminal, lancez une surveillance :

kubectl get pods -l app=web -w

Dans le premier terminal :

kubectl scale deployment web --replicas=4
kubectl set image deployment/web nginx=nginx:1.28
kubectl annotate deployment/web kubernetes.io/change-cause="passage en nginx 1.28"
kubectl rollout status deployment/web

Vérifiez la version :

kubectl get replicasets
kubectl describe deployment web | grep Image

Il existe maintenant deux ReplicaSets : l'ancien à 0 réplica, le nouveau à 4.

4.5 Historique et retour arrière

Provoquez une mise à jour ratée avec une image inexistante :

kubectl set image deployment/web nginx=nginx:9.99-inexistant
kubectl annotate deployment/web kubernetes.io/change-cause="version cassée" --overwrite
kubectl rollout status deployment/web --timeout=30s
kubectl get pods -l app=web

Certains pods sont en ErrImagePull ou ImagePullBackOff, mais les anciens pods continuent de tourner. Revenez en arrière :

kubectl rollout history deployment/web
kubectl rollout undo deployment/web
kubectl rollout status deployment/web
kubectl describe deployment web | grep Image

4.6 Passer au déclaratif

Exportez le Deployment dans un fichier propre :

kubectl create deployment web-decl --image=nginx:1.28 --replicas=2 --dry-run=client -o yaml > deploy-web.yaml

Éditez deploy-web.yaml pour qu'il ressemble à ceci :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-decl
  labels:
    app: web-decl
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web-decl
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: web-decl
    spec:
      containers:
        - name: nginx
          image: nginx:1.28
          ports:
            - containerPort: 80
kubectl apply -f deploy-web.yaml

Modifiez replicas: 2 en replicas: 4 dans le fichier, puis :

kubectl diff -f deploy-web.yaml
kubectl apply -f deploy-web.yaml
kubectl get deployment web-decl

❓ Questions partie 4

  1. Pourquoi ne crée-t-on presque jamais de ReplicaSet directement ?
  2. Que garantissent maxSurge: 1 et maxUnavailable: 0 pendant une mise à jour ?
  3. Que se passe-t-il si les labels de selector.matchLabels ne correspondent pas à ceux du template ?

Partie 5 : Services et DNS

5.1 Une application qui affiche son nom

L'image traefik/whoami renvoie le nom du pod qui répond, ce qui permet de voir la répartition de charge.

kubectl create deployment whoami --image=traefik/whoami --replicas=3
kubectl rollout status deployment/whoami

5.2 Service ClusterIP

kubectl expose deployment whoami --port=80 --target-port=80
kubectl get service whoami
kubectl get endpointslices -l kubernetes.io/service-name=whoami

Les EndpointSlices listent les IP des pods derrière le Service. Comparez avec :

kubectl get pods -l app=whoami -o wide

5.3 Tester depuis l'intérieur du cluster

kubectl run test --rm -it --image=busybox:1.36 --restart=Never -- \
  sh -c 'for i in 1 2 3 4 5 6 7 8; do wget -qO- http://whoami | grep Hostname; done'

Plusieurs noms de pods différents apparaissent : le Service répartit les requêtes.

5.4 Résolution DNS

kubectl run dns --rm -it --image=busybox:1.36 --restart=Never -- nslookup whoami.tp.svc.cluster.local

Formats de noms utilisables :

Depuis Nom
Le même namespace whoami
Un autre namespace whoami.tp
Nom complet whoami.tp.svc.cluster.local

5.5 Service NodePort

Créez svc-nodeport.yaml :

apiVersion: v1
kind: Service
metadata:
  name: whoami-np
spec:
  type: NodePort
  selector:
    app: whoami
  ports:
    - port: 80
      targetPort: 80
      nodePort: 30080
kubectl apply -f svc-nodeport.yaml
kubectl get service whoami-np
for i in 1 2 3 4 5; do curl -s http://localhost:30080 | grep Hostname; done

Ceci fonctionne grâce au extraPortMappings défini à la création du cluster.

5.6 Port-forward vers un Service

kubectl port-forward service/whoami 8081:80

Dans un autre terminal :

curl -s http://localhost:8081

5.7 Service headless (aperçu)

kubectl create service clusterip whoami-headless --clusterip=None --tcp=80:80
kubectl patch service whoami-headless -p '{"spec":{"selector":{"app":"whoami"}}}'
kubectl run dns2 --rm -it --image=busybox:1.36 --restart=Never -- nslookup whoami-headless

Le DNS renvoie directement les IP des pods, sans IP virtuelle. On s'en servira avec le StatefulSet du mini-projet.

❓ Questions partie 5

  1. Pourquoi ne faut-il pas contacter les pods directement par leur IP ?
  2. Dans quelle plage doit se trouver un nodePort par défaut ?
  3. Que renvoie la résolution DNS d'un Service headless ?

Partie 6 : ConfigMaps et Secrets

6.1 ConfigMap en variables d'environnement

kubectl create configmap app-config \
  --from-literal=APP_MODE=production \
  --from-literal=APP_COULEUR=bleu
kubectl get configmap app-config -o yaml

Créez pod-env.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: env-demo
spec:
  restartPolicy: Never
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "env | grep APP_; sleep 3600"]
      envFrom:
        - configMapRef:
            name: app-config
kubectl apply -f pod-env.yaml
kubectl logs env-demo

6.2 ConfigMap montée en fichier

On remplace la page d'accueil de nginx par un fichier issu d'une ConfigMap.

cat > index.html <<'EOF'
<html><body><h1>Page servie depuis une ConfigMap</h1></body></html>
EOF
kubectl create configmap page-accueil --from-file=index.html

Créez deploy-page.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: site
spec:
  replicas: 1
  selector:
    matchLabels:
      app: site
  template:
    metadata:
      labels:
        app: site
    spec:
      containers:
        - name: nginx
          image: nginx:1.28
          volumeMounts:
            - name: html
              mountPath: /usr/share/nginx/html
      volumes:
        - name: html
          configMap:
            name: page-accueil
kubectl apply -f deploy-page.yaml
kubectl rollout status deployment/site
kubectl exec deploy/site -- cat /usr/share/nginx/html/index.html

Modifiez la ConfigMap :

kubectl create configmap page-accueil \
  --from-literal=index.html='<html><body><h1>Version 2</h1></body></html>' \
  --dry-run=client -o yaml | kubectl apply -f -

Attendez environ une minute puis relancez :

kubectl exec deploy/site -- cat /usr/share/nginx/html/index.html

Le fichier monté est mis à jour automatiquement (ce n'est pas le cas des variables d'environnement, qui nécessitent un redémarrage du pod).

6.3 Secrets

kubectl create secret generic db-credentials \
  --from-literal=DB_USER=admin \
  --from-literal=DB_PASSWORD='S3cr3t!'
kubectl get secret db-credentials -o yaml

Les valeurs sont encodées en base64, pas chiffrées :

kubectl get secret db-credentials -o jsonpath='{.data.DB_PASSWORD}' | base64 -d; echo

Utilisation dans un pod, pod-secret.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: secret-demo
spec:
  restartPolicy: Never
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "echo Utilisateur=$DB_USER; ls /secrets; sleep 3600"]
      env:
        - name: DB_USER
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: DB_USER
      volumeMounts:
        - name: creds
          mountPath: /secrets
          readOnly: true
  volumes:
    - name: creds
      secret:
        secretName: db-credentials
kubectl apply -f pod-secret.yaml
kubectl logs secret-demo
kubectl exec secret-demo -- cat /secrets/DB_PASSWORD; echo

6.4 Nettoyage

kubectl delete pod env-demo secret-demo

❓ Questions partie 6

  1. Un Secret Kubernetes est-il chiffré par défaut ? Que faut-il mettre en place pour le protéger ?
  2. Pourquoi un changement de ConfigMap n'est-il pas visible dans une variable d'environnement déjà chargée ?

Partie 7 : Probes et ressources

7.1 Readiness probe

Créez deploy-probe.yaml avec une readiness probe volontairement fausse :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: probe-demo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: probe-demo
  template:
    metadata:
      labels:
        app: probe-demo
    spec:
      containers:
        - name: nginx
          image: nginx:1.28
          readinessProbe:
            httpGet:
              path: /inexistant
              port: 80
            initialDelaySeconds: 3
            periodSeconds: 5
kubectl apply -f deploy-probe.yaml
kubectl expose deployment probe-demo --port=80
sleep 15
kubectl get pods -l app=probe-demo
kubectl get endpointslices -l kubernetes.io/service-name=probe-demo
kubectl describe pod -l app=probe-demo | grep -i readiness

Les pods sont Running mais READY 0/1, et le Service n'a aucun endpoint prêt. Corrigez path: /inexistant en path: /, puis :

kubectl apply -f deploy-probe.yaml
kubectl rollout status deployment/probe-demo
kubectl get endpointslices -l kubernetes.io/service-name=probe-demo

7.2 Liveness probe

Créez pod-liveness.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: liveness-demo
spec:
  containers:
    - name: demo
      image: busybox:1.36
      command: ["sh", "-c", "touch /tmp/sain; sleep 30; rm -f /tmp/sain; sleep 600"]
      livenessProbe:
        exec:
          command: ["cat", "/tmp/sain"]
        initialDelaySeconds: 5
        periodSeconds: 5
kubectl apply -f pod-liveness.yaml
kubectl get pod liveness-demo -w

Au bout d'environ 45 secondes, la colonne RESTARTS augmente. Vérifiez :

kubectl describe pod liveness-demo | tail -15

7.3 Requests et limits

Créez pod-resources.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: resources-demo
spec:
  containers:
    - name: nginx
      image: nginx:1.28
      resources:
        requests:
          cpu: "100m"
          memory: "64Mi"
        limits:
          cpu: "250m"
          memory: "128Mi"
kubectl apply -f pod-resources.yaml
kubectl describe pod resources-demo | grep -A6 -E "Limits|Requests"
kubectl get pod resources-demo -o jsonpath='{.status.qosClass}'; echo

Maintenant une demande impossible à satisfaire :

kubectl run trop-gros --image=nginx:1.28 \
  --overrides='{"spec":{"containers":[{"name":"trop-gros","image":"nginx:1.28","resources":{"requests":{"cpu":"64"}}}]}}'
kubectl get pod trop-gros
kubectl describe pod trop-gros | tail -5

Le pod reste Pending avec un message Insufficient cpu.

7.4 Dépassement mémoire

kubectl run oom --image=busybox:1.36 --restart=Never \
  --overrides='{"spec":{"containers":[{"name":"oom","image":"busybox:1.36","command":["sh","-c","head -c 200m /dev/zero | tail"],"resources":{"limits":{"memory":"50Mi"}}}]}}'
sleep 10
kubectl get pod oom
kubectl get pod oom -o jsonpath='{.status.containerStatuses[0].state.terminated.reason}'; echo

Résultat attendu : OOMKilled.

7.5 Nettoyage

kubectl delete pod liveness-demo resources-demo trop-gros oom

❓ Questions partie 7

  1. Quelle est la différence de conséquence entre l'échec d'une readiness probe et l'échec d'une liveness probe ?
  2. Quelle ressource sert au scheduler pour placer un pod : requests ou limits ?
  3. Quelles sont les trois classes de QoS et à quoi correspond resources-demo ?

Partie 8 : Stockage persistant

8.1 StorageClass disponible

kubectl get storageclass
kubectl describe storageclass standard

kind fournit une StorageClass standard qui crée des volumes locaux sur le nœud. Notez le mode VolumeBindingMode: WaitForFirstConsumer.

8.2 Créer un PVC

Créez pvc.yaml :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: donnees
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 1Gi
kubectl apply -f pvc.yaml
kubectl get pvc

Le PVC est Pending : aucun pod ne l'utilise encore.

8.3 Utiliser le PVC

Créez pod-pvc.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: stockage
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "sleep 3600"]
      volumeMounts:
        - name: vol
          mountPath: /data
  volumes:
    - name: vol
      persistentVolumeClaim:
        claimName: donnees
kubectl apply -f pod-pvc.yaml
kubectl wait --for=condition=Ready pod/stockage --timeout=60s
kubectl get pvc,pv

Le PVC est maintenant Bound et un PV a été créé dynamiquement.

8.4 Vérifier la persistance

kubectl exec stockage -- sh -c 'echo "donnee importante" > /data/fichier.txt'
kubectl delete pod stockage
kubectl apply -f pod-pvc.yaml
kubectl wait --for=condition=Ready pod/stockage --timeout=60s
kubectl exec stockage -- cat /data/fichier.txt

Le fichier a survécu à la suppression du pod.

8.5 Nettoyage

kubectl delete pod stockage
kubectl delete pvc donnees
kubectl get pv

❓ Questions partie 8

  1. Pourquoi le PVC restait-il Pending avant la création du pod ?
  2. Que devient le PV après suppression du PVC, et de quel paramètre cela dépend-il ?
  3. Que signifie ReadWriteOnce ?

Partie 9 : Jobs et CronJobs

9.1 Job simple

kubectl create job bonjour --image=busybox:1.36 -- \
  sh -c 'for i in 1 2 3; do echo "etape $i"; sleep 2; done; echo termine'
kubectl get jobs -w

Une fois COMPLETIONS 1/1 :

kubectl logs job/bonjour
kubectl get pods -l job-name=bonjour

9.2 Job parallèle

Créez job-parallele.yaml :

apiVersion: batch/v1
kind: Job
metadata:
  name: calcul
spec:
  completions: 6
  parallelism: 2
  backoffLimit: 2
  ttlSecondsAfterFinished: 300
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: calcul
          image: busybox:1.36
          command: ["sh", "-c", "echo traitement sur $(hostname); sleep 5"]
kubectl apply -f job-parallele.yaml
kubectl get pods -l job-name=calcul -w

Les pods s'exécutent deux par deux jusqu'à six complétions.

9.3 Job en échec

kubectl create job echec --image=busybox:1.36 -- sh -c 'echo erreur; exit 1'
sleep 60
kubectl get job echec
kubectl describe job echec | tail -8

Le Job réessaie jusqu'à backoffLimit (6 par défaut) avec un délai croissant.

9.4 CronJob

kubectl create cronjob horloge --image=busybox:1.36 --schedule="*/1 * * * *" -- date
kubectl get cronjob

Attendez deux ou trois minutes :

kubectl get jobs
kubectl logs job/$(kubectl get jobs -o name | grep horloge | tail -1 | cut -d/ -f2)

Suspendre puis supprimer le CronJob :

kubectl patch cronjob horloge -p '{"spec":{"suspend":true}}'
kubectl delete cronjob horloge
kubectl delete job bonjour echec calcul --ignore-not-found

❓ Questions partie 9

  1. Pourquoi restartPolicy: Always est-il interdit dans un Job ?
  2. À quoi sert ttlSecondsAfterFinished ?

Partie 10 : Helm et Ingress

10.1 Installer Traefik avec Helm

helm repo add traefik https://traefik.github.io/charts
helm repo update
helm install traefik traefik/traefik --namespace traefik --create-namespace
kubectl -n traefik rollout status deployment/traefik
kubectl -n traefik get pods,svc

Le Service traefik est de type LoadBalancer. Sur kind, son EXTERNAL-IP reste <pending> car il n'y a pas de fournisseur de load balancer : on passera par un port-forward.

10.2 Commandes Helm utiles

helm list -A
helm status traefik -n traefik
helm get values traefik -n traefik
helm show values traefik/traefik | less
kubectl get ingressclass

10.3 Créer un Ingress

On réutilise le Service whoami de la partie 5. Créez ingress-whoami.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: whoami
spec:
  ingressClassName: traefik
  rules:
    - host: whoami.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80
kubectl apply -f ingress-whoami.yaml
kubectl get ingress

10.4 Tester

Dans un terminal dédié :

kubectl -n traefik port-forward service/traefik 8080:80

Dans un autre terminal :

curl -s -H "Host: whoami.local" http://localhost:8080 | grep Hostname
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: autre.local" http://localhost:8080

La première requête est routée vers whoami. La seconde renvoie 404 car aucun Ingress ne correspond à cet hôte.

10.5 Routage par chemin

Ajoutez une seconde règle vers le Service site de la partie 6. Il faut d'abord l'exposer :

kubectl expose deployment site --port=80

Créez ingress-chemins.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: chemins
spec:
  ingressClassName: traefik
  rules:
    - host: tp.local
      http:
        paths:
          - path: /whoami
            pathType: Prefix
            backend:
              service:
                name: whoami
                port:
                  number: 80
          - path: /
            pathType: Prefix
            backend:
              service:
                name: site
                port:
                  number: 80
kubectl apply -f ingress-chemins.yaml
curl -s -H "Host: tp.local" http://localhost:8080/
curl -s -H "Host: tp.local" http://localhost:8080/whoami | grep Hostname

❓ Questions partie 10

  1. Quelle est la différence entre un Ingress et un Ingress Controller ?
  2. Que se passerait-il si aucun Ingress Controller n'était installé ?

Partie 11 : RBAC et ServiceAccounts

11.1 Créer un compte de service en lecture seule

kubectl create serviceaccount lecteur
kubectl create role lecture-pods --verb=get,list,watch --resource=pods,pods/log
kubectl create rolebinding lecteur-pods --role=lecture-pods --serviceaccount=tp:lecteur

11.2 Vérifier les droits

kubectl auth can-i list pods --as=system:serviceaccount:tp:lecteur
kubectl auth can-i delete pods --as=system:serviceaccount:tp:lecteur
kubectl auth can-i list secrets --as=system:serviceaccount:tp:lecteur
kubectl auth can-i list pods -n kube-system --as=system:serviceaccount:tp:lecteur

Résultats attendus : yes, no, no, no.

11.3 Utiliser le compte depuis un pod

Créez pod-rbac.yaml :

apiVersion: v1
kind: Pod
metadata:
  name: kubectl-pod
spec:
  serviceAccountName: lecteur
  containers:
    - name: kubectl
      image: bitnami/kubectl:latest
      command: ["sleep", "3600"]
kubectl apply -f pod-rbac.yaml
kubectl wait --for=condition=Ready pod/kubectl-pod --timeout=90s
kubectl exec kubectl-pod -- kubectl get pods
kubectl exec kubectl-pod -- kubectl delete pod site-xxx 2>&1 | tail -1
kubectl exec kubectl-pod -- kubectl get secrets 2>&1 | tail -1

Les deux dernières commandes renvoient Forbidden.

Si l'image bitnami/kubectl n'est pas disponible dans votre environnement, remplacez-la par registry.k8s.io/kubectl:v1.34.0 (ou la version correspondant à votre cluster, visible avec kubectl version).

11.4 Étendre les droits

Ajoutez la lecture des Deployments au rôle :

kubectl get role lecture-pods -o yaml > role-lecture.yaml

Éditez role-lecture.yaml pour ajouter une règle :

rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch"]
kubectl apply -f role-lecture.yaml
kubectl exec kubectl-pod -- kubectl get deployments
kubectl delete pod kubectl-pod

❓ Questions partie 11

  1. Quelle est la différence entre un Role et un ClusterRole ?
  2. Pourquoi la règle deployments est-elle dans l'apiGroup apps et non "" ?

Partie 12 : Namespaces et quotas

12.1 Créer un namespace limité

kubectl create namespace equipe-a

Créez quota.yaml :

apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota-equipe-a
  namespace: equipe-a
spec:
  hard:
    pods: "3"
    requests.cpu: "500m"
    requests.memory: "512Mi"
    limits.cpu: "1"
    limits.memory: "1Gi"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: defauts
  namespace: equipe-a
spec:
  limits:
    - type: Container
      default:
        cpu: "200m"
        memory: "128Mi"
      defaultRequest:
        cpu: "100m"
        memory: "64Mi"
kubectl apply -f quota.yaml
kubectl describe namespace equipe-a

12.2 Tester le quota

kubectl -n equipe-a create deployment quota-test --image=nginx:1.28 --replicas=5
sleep 10
kubectl -n equipe-a get pods
kubectl -n equipe-a describe resourcequota quota-equipe-a
kubectl -n equipe-a get events --sort-by=.lastTimestamp | tail -5

Seuls 3 pods sont créés. Les événements du ReplicaSet mentionnent exceeded quota.

Vérifiez que la LimitRange a injecté des valeurs par défaut :

kubectl -n equipe-a get pod -l app=quota-test -o jsonpath='{.items[0].spec.containers[0].resources}'; echo

12.3 Nettoyage

kubectl delete namespace equipe-a

Supprimer un namespace supprime tout ce qu'il contient.

❓ Questions partie 12

  1. Pourquoi une LimitRange est-elle presque indispensable quand un ResourceQuota porte sur le CPU et la mémoire ?
  2. Les nœuds et les PersistentVolumes appartiennent-ils à un namespace ?

Partie 13 : Atelier dépannage

Cette partie ne donne pas les solutions directement. Déployez les ressources cassées, trouvez la cause de chaque problème et corrigez-la.

Créez casse.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: casse-1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: casse-1
  template:
    metadata:
      labels:
        app: casse-1
    spec:
      containers:
        - name: web
          image: nginx:1.99-nexistepas
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: casse-2
spec:
  replicas: 1
  selector:
    matchLabels:
      app: casse-2
  template:
    metadata:
      labels:
        app: casse-2
    spec:
      containers:
        - name: app
          image: busybox:1.36
          command: ["sh", "-c", "echo demarrage && cat /config/app.conf && sleep 3600"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: casse-3
spec:
  replicas: 2
  selector:
    matchLabels:
      app: casse-3
  template:
    metadata:
      labels:
        app: casse-3
    spec:
      containers:
        - name: web
          image: traefik/whoami
---
apiVersion: v1
kind: Service
metadata:
  name: casse-3
spec:
  selector:
    app: casse3
  ports:
    - port: 80
      targetPort: 80
---
apiVersion: v1
kind: Pod
metadata:
  name: casse-4
spec:
  containers:
    - name: app
      image: busybox:1.36
      command: ["sh", "-c", "echo $MOT_DE_PASSE; sleep 3600"]
      env:
        - name: MOT_DE_PASSE
          valueFrom:
            secretKeyRef:
              name: secret-absent
              key: password
kubectl apply -f casse.yaml
sleep 30
kubectl get pods

Méthode de diagnostic

Pour chaque ressource, suivez cet ordre :

kubectl get pods
kubectl describe pod <nom>
kubectl logs <nom>
kubectl logs <nom> --previous
kubectl get events --sort-by=.lastTimestamp
kubectl get endpointslices -l kubernetes.io/service-name=<service>

Travail demandé

Ressource Symptôme à identifier Cause Correction
casse-1
casse-2
casse-3
casse-4

Pour tester casse-3 une fois corrigé :

kubectl run t --rm -it --image=busybox:1.36 --restart=Never -- wget -qO- http://casse-3

Les corrections sont en annexe A (questions 31 à 34). Nettoyez ensuite :

kubectl delete -f casse.yaml --ignore-not-found

Partie 14 : Mini-projet final

Objectif

Déployer dans un namespace projet une base PostgreSQL et l'interface web Adminer, accessible via l'Ingress sur http://adminer.local:8080.

Cahier des charges

  1. Un namespace projet.
  2. Un Secret postgres-secret contenant POSTGRES_USER=tp, POSTGRES_PASSWORD (valeur de votre choix) et POSTGRES_DB=tpdb.
  3. Un StatefulSet postgres :
    • image postgres:16
    • 1 réplica
    • variables d'environnement issues du Secret
    • variable PGDATA=/var/lib/postgresql/data/pgdata
    • un volume persistant de 1 Gi via volumeClaimTemplates, monté sur /var/lib/postgresql/data
    • une readiness probe de type exec avec la commande pg_isready -U tp -d tpdb
    • requests : 100m CPU, 256Mi mémoire ; limits : 500m CPU, 512Mi mémoire
  4. Un Service headless postgres sur le port 5432.
  5. Un Deployment adminer :
    • image adminer:latest
    • 2 réplicas
    • variable ADMINER_DEFAULT_SERVER=postgres
    • readiness probe HTTP sur le port 8080
  6. Un Service ClusterIP adminer (port 80 vers 8080).
  7. Un Ingress adminer pour l'hôte adminer.local, classe traefik.

Étapes suggérées

  1. Écrire chaque manifeste dans un dossier projet/.
  2. Appliquer le tout avec kubectl apply -f projet/.
  3. Vérifier chaque ressource avec kubectl -n projet get all,pvc,ingress.
  4. Ouvrir http://adminer.local:8080 dans un navigateur et se connecter (système PostgreSQL, serveur postgres, utilisateur tp, base tpdb).

Lancer le port-forward Traefik si ce n'est plus le cas :

kubectl -n traefik port-forward service/traefik 8080:80

Ajouter l'hôte dans /etc/hosts :

echo "127.0.0.1 adminer.local" | sudo tee -a /etc/hosts

Validation

  • [ ] Créer une table et insérer une ligne depuis Adminer.
  • [ ] Supprimer le pod postgres-0 et vérifier que la table existe toujours après son redémarrage.
  • [ ] Passer Adminer à 3 réplicas sans interruption de service.

[ ] Se connecter en ligne de commande :

kubectl -n projet exec -it postgres-0 -- psql -U tp -d tpdb -c '\dt'

Bonus

  • Ajouter un CronJob qui exécute pg_dump toutes les 5 minutes vers un PVC dédié.
  • Créer un ServiceAccount et un Role qui n'autorisent que la lecture des pods du namespace projet.
  • Ajouter un ResourceQuota au namespace.

La solution complète est en annexe B.


Partie 15 : Nettoyage

kubectl config set-context --current --namespace=default
kind delete cluster --name tp-k8s
sudo sed -i '/adminer.local/d' /etc/hosts

Annexe A : Corrigé des questions

1. Sur tp-k8s-control-plane. Ce sont des composants du plan de contrôle, lancés comme pods statiques par le kubelet du nœud control-plane à partir des manifestes de /etc/kubernetes/manifests.

2. Trois, un par nœud. kube-proxy est déployé par un DaemonSet, qui place un pod sur chaque nœud.

3. get affiche une liste synthétique (ou l'objet complet avec -o yaml). describe produit une vue lisible et détaillée, incluant les événements liés à l'objet.

4. Deux conteneurs sur deux sont prêts dans le pod.

5. Non. Un pod nu n'est géré par aucun contrôleur : une fois supprimé, il disparaît.

6. Oui. Les conteneurs d'un même pod partagent le même espace réseau, donc la même IP. Ils communiquent entre eux via localhost.

7. Les labels servent à sélectionner et regrouper des objets (Services, Deployments, requêtes -l). Les annotations stockent des informations descriptives ou destinées à des outils, sans rôle dans la sélection.

8. kubectl delete pods -l env=prod

9. Le Deployment gère les ReplicaSets à notre place et ajoute les mises à jour progressives, l'historique et le retour arrière.

10. Au plus un pod supplémentaire est créé pendant la mise à jour, et aucun pod n'est retiré avant qu'un nouveau soit prêt. La capacité ne descend jamais sous le nombre de réplicas demandé.

11. L'API refuse le Deployment avec une erreur de validation (selector does not match template labels).

12. Les pods sont éphémères : leur IP change à chaque recréation. Le Service fournit une IP et un nom DNS stables, et répartit la charge.

13. 30000 à 32767.

14. Directement les IP des pods sélectionnés (un enregistrement A par pod prêt), sans IP de Service.

15. Non, il est seulement encodé en base64 et stocké tel quel dans etcd. Il faut activer le chiffrement au repos (EncryptionConfiguration), restreindre l'accès par RBAC, et idéalement utiliser un gestionnaire externe (Vault, External Secrets, Sealed Secrets).

16. Les variables d'environnement sont injectées au démarrage du processus. Il faut redémarrer les pods (kubectl rollout restart deployment/<nom>).

17. Readiness : le pod est retiré des endpoints du Service, il ne reçoit plus de trafic, mais n'est pas redémarré. Liveness : le kubelet redémarre le conteneur.

18. requests. Les limits sont appliquées à l'exécution par le kubelet et le runtime.

19. Guaranteed (requests égales aux limits pour tous les conteneurs), Burstable (au moins une request ou limit définie sans être Guaranteed), BestEffort (aucune). resources-demo est Burstable.

20. La StorageClass utilise WaitForFirstConsumer : le volume n'est créé qu'une fois le pod planifié, pour être placé sur le bon nœud.

21. Il dépend de la reclaimPolicy de la StorageClass (ou du PV). Avec Delete (cas de kind), le PV et les données sont supprimés. Avec Retain, le PV est conservé en état Released.

22. Le volume peut être monté en lecture-écriture par un seul nœud à la fois.

23. Un Job doit pouvoir se terminer. Avec Always, le conteneur serait relancé indéfiniment. Seuls Never et OnFailure sont acceptés.

24. À supprimer automatiquement le Job et ses pods un certain temps après sa fin.

25. L'Ingress est une ressource déclarative décrivant des règles de routage HTTP. L'Ingress Controller (Traefik ici) est le logiciel qui lit ces règles et route réellement le trafic.

26. L'objet Ingress serait créé mais n'aurait aucun effet : personne ne l'appliquerait.

27. Un Role s'applique à un namespace. Un ClusterRole s'applique à tout le cluster ou à des ressources non namespacées (nœuds, PV), et peut aussi être lié dans un seul namespace via un RoleBinding.

28. Les Deployments font partie du groupe d'API apps (apps/v1). Le groupe "" correspond au groupe core (v1) : pods, services, configmaps, secrets.

29. Quand un quota porte sur requests ou limits, tout pod qui ne les déclare pas est refusé. La LimitRange injecte des valeurs par défaut et évite ces refus.

30. Non, ce sont des ressources à portée cluster. On peut le vérifier avec kubectl api-resources --namespaced=false.

31. casse-1. Symptôme : ErrImagePull / ImagePullBackOff. Cause : le tag 1.99-nexistepas n'existe pas. Correction :

kubectl set image deployment/casse-1 web=nginx:1.28

32. casse-2. Symptôme : CrashLoopBackOff, les logs montrent cat: can't open '/config/app.conf'. Cause : le fichier /config/app.conf n'existe pas, cat échoue, le && empêche le sleep de s'exécuter et le conteneur se termine en erreur. Correction : fournir le fichier via une ConfigMap.

kubectl create configmap casse-2-config --from-literal=app.conf='parametre=valeur'
kubectl patch deployment casse-2 --type=json -p='[
  {"op":"add","path":"/spec/template/spec/volumes","value":[{"name":"config","configMap":{"name":"casse-2-config"}}]},
  {"op":"add","path":"/spec/template/spec/containers/0/volumeMounts","value":[{"name":"config","mountPath":"/config"}]}
]'
Variante à proposer : remplacer les && par des ;. Le sleep s'exécute alors malgré l'échec de cat et le pod reste Running alors que l'application est mal configurée. Cela montre qu'un statut Running ne suffit pas à valider un déploiement.

33. casse-3. Symptôme : les pods tournent mais le Service ne répond pas, et kubectl get endpointslices -l kubernetes.io/service-name=casse-3 ne montre aucune adresse. Cause : le sélecteur du Service (app: casse3) ne correspond pas au label des pods (app: casse-3). Correction :

kubectl patch service casse-3 -p '{"spec":{"selector":{"app":"casse-3"}}}'

34. casse-4. Symptôme : CreateContainerConfigError, describe indique secret "secret-absent" not found. Correction :

kubectl create secret generic secret-absent --from-literal=password=demo
kubectl get pod casse-4 -w

Le pod démarre automatiquement une fois le Secret présent.


Annexe B : Solution du mini-projet

Fichier projet/00-namespace.yaml :

apiVersion: v1
kind: Namespace
metadata:
  name: projet

Fichier projet/10-secret.yaml :

apiVersion: v1
kind: Secret
metadata:
  name: postgres-secret
  namespace: projet
type: Opaque
stringData:
  POSTGRES_USER: tp
  POSTGRES_PASSWORD: ChangezMoi123
  POSTGRES_DB: tpdb

Fichier projet/20-postgres.yaml :

apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: projet
spec:
  clusterIP: None
  selector:
    app: postgres
  ports:
    - name: postgres
      port: 5432
      targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: postgres
  namespace: projet
spec:
  serviceName: postgres
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          envFrom:
            - secretRef:
                name: postgres-secret
          env:
            - name: PGDATA
              value: /var/lib/postgresql/data/pgdata
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "tp", "-d", "tpdb"]
            initialDelaySeconds: 5
            periodSeconds: 5
          resources:
            requests:
              cpu: "100m"
              memory: "256Mi"
            limits:
              cpu: "500m"
              memory: "512Mi"
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
    - metadata:
        name: data
      spec:
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 1Gi

Fichier projet/30-adminer.yaml :

apiVersion: apps/v1
kind: Deployment
metadata:
  name: adminer
  namespace: projet
spec:
  replicas: 2
  selector:
    matchLabels:
      app: adminer
  template:
    metadata:
      labels:
        app: adminer
    spec:
      containers:
        - name: adminer
          image: adminer:latest
          ports:
            - containerPort: 8080
          env:
            - name: ADMINER_DEFAULT_SERVER
              value: postgres
          readinessProbe:
            httpGet:
              path: /
              port: 8080
            initialDelaySeconds: 3
            periodSeconds: 5
          resources:
            requests:
              cpu: "50m"
              memory: "64Mi"
            limits:
              cpu: "250m"
              memory: "128Mi"
---
apiVersion: v1
kind: Service
metadata:
  name: adminer
  namespace: projet
spec:
  selector:
    app: adminer
  ports:
    - port: 80
      targetPort: 8080

Fichier projet/40-ingress.yaml :

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: adminer
  namespace: projet
spec:
  ingressClassName: traefik
  rules:
    - host: adminer.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: adminer
                port:
                  number: 80

Déploiement et vérifications :

kubectl apply -f projet/00-namespace.yaml
kubectl apply -f projet/
kubectl -n projet rollout status statefulset/postgres
kubectl -n projet rollout status deployment/adminer
kubectl -n projet get all,pvc,ingress
curl -s -o /dev/null -w "%{http_code}\n" -H "Host: adminer.local" http://localhost:8080

Test de persistance :

kubectl -n projet exec -it postgres-0 -- psql -U tp -d tpdb -c "CREATE TABLE IF NOT EXISTS test (id int); INSERT INTO test VALUES (1);"
kubectl -n projet delete pod postgres-0
kubectl -n projet wait --for=condition=Ready pod/postgres-0 --timeout=120s
kubectl -n projet exec -it postgres-0 -- psql -U tp -d tpdb -c "SELECT * FROM test;"

Mise à l'échelle d'Adminer :

kubectl -n projet scale deployment adminer --replicas=3
kubectl -n projet get pods -l app=adminer

Bonus sauvegarde, fichier projet/50-backup.yaml :

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: backups
  namespace: projet
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 1Gi
---
apiVersion: batch/v1
kind: CronJob
metadata:
  name: pg-backup
  namespace: projet
spec:
  schedule: "*/5 * * * *"
  successfulJobsHistoryLimit: 2
  failedJobsHistoryLimit: 2
  jobTemplate:
    spec:
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: backup
              image: postgres:16
              env:
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef:
                      name: postgres-secret
                      key: POSTGRES_PASSWORD
              command:
                - sh
                - -c
                - pg_dump -h postgres -U tp tpdb > /backups/tpdb-$(date +%Y%m%d-%H%M%S).sql && ls -l /backups
              volumeMounts:
                - name: backups
                  mountPath: /backups
          volumes:
            - name: backups
              persistentVolumeClaim:
                claimName: backups
kubectl apply -f projet/50-backup.yaml
kubectl -n projet create job backup-manuel --from=cronjob/pg-backup
kubectl -n projet logs job/backup-manuel -f

Annexe C : Aide-mémoire kubectl

Consultation

kubectl get <ressource>                     # lister
kubectl get <ressource> -o wide             # plus de colonnes
kubectl get <ressource> -o yaml             # objet complet
kubectl get all -n <ns>                     # principales ressources d'un namespace
kubectl get pods -A                         # tous les namespaces
kubectl get pods -l app=web                 # filtre par label
kubectl get pods -w                         # suivi en direct
kubectl describe <ressource> <nom>          # détail + événements
kubectl get events --sort-by=.lastTimestamp # événements récents
kubectl explain <ressource>.<champ>         # documentation intégrée
kubectl api-resources                       # types disponibles

Création et modification

kubectl apply -f <fichier|dossier>          # créer ou mettre à jour
kubectl diff -f <fichier>                   # prévisualiser un changement
kubectl delete -f <fichier>                 # supprimer
kubectl create deployment <nom> --image=<img> --dry-run=client -o yaml
kubectl run <nom> --image=<img> --dry-run=client -o yaml
kubectl expose deployment <nom> --port=80 --dry-run=client -o yaml
kubectl edit <ressource> <nom>              # édition directe
kubectl patch <ressource> <nom> -p '<json>'
kubectl label <ressource> <nom> cle=valeur
kubectl annotate <ressource> <nom> cle=valeur

Deployments

kubectl scale deployment <nom> --replicas=N
kubectl set image deployment/<nom> <conteneur>=<image>
kubectl rollout status deployment/<nom>
kubectl rollout history deployment/<nom>
kubectl rollout undo deployment/<nom>
kubectl rollout restart deployment/<nom>

Débogage

kubectl logs <pod> [-c <conteneur>] [-f] [--previous]
kubectl logs deploy/<nom>
kubectl exec -it <pod> -- sh
kubectl port-forward pod/<pod> 8080:80
kubectl port-forward service/<svc> 8080:80
kubectl run tmp --rm -it --image=busybox:1.36 --restart=Never -- sh
kubectl get endpointslices -l kubernetes.io/service-name=<svc>
kubectl auth can-i <verbe> <ressource> --as=<utilisateur>
kubectl top pods                            # nécessite metrics-server

Contexte et namespace

kubectl config get-contexts
kubectl config use-context <contexte>
kubectl config set-context --current --namespace=<ns>

Statuts de pod fréquents

Statut Signification habituelle Premier réflexe
Pending Pas de nœud disponible ou PVC non lié describe pod, section Events
ContainerCreating Téléchargement d'image ou montage de volume en cours Attendre, puis describe pod
ErrImagePull / ImagePullBackOff Image ou tag introuvable, registre privé sans identifiants Vérifier le nom de l'image
CrashLoopBackOff Le conteneur démarre puis s'arrête en boucle logs --previous
CreateContainerConfigError ConfigMap ou Secret référencé absent describe pod
OOMKilled Limite mémoire dépassée Augmenter la limite ou corriger l'application
Running mais 0/1 Readiness probe en échec describe pod, vérifier la probe
Completed Processus terminé avec le code 0 (normal pour un Job) Aucun