Aller au contenu

Lab : Signalements sur Kubernetes

200 Pratiquer ⏱ 4 h kuberneteskubectlkind

Prérequis

Testé avec kubectl 1.36.0 kubernetes 1.36 , vérifié le 5 octobre 2026

Ce lab rassemble le cours Kubernetes : les fondamentaux. Vous déployez Signalements sur le cluster kind formation de la leçon 3, avec tout ce que les leçons ont ajouté au simple « ça tourne », puis un script vérifie dans le cluster que le déploiement a les propriétés attendues. Il valide le niveau 200 des compétences « Kubernetes : objets fondamentaux », « Kubernetes : réseau et exposition » et « Kubernetes : stockage ».

Comptez une demi-journée. La leçon 11 assemble presque tout : essayez d'écrire chaque manifeste avant de l'ouvrir.

Ce qu'il faut

  • Le cluster kind formation (un plan de contrôle et deux nœuds), créé à la leçon 3 avec un kubeconfig dédié, et une implémentation de Gateway API ou un contrôleur d'Ingress installé comme à la leçon 11.
  • kubectl 1.36 et jq.

Les exigences

ExigenceLeçon
Tout vit dans le namespace signalements3
L'adresse de la base est fournie à l'application par un Secret, référencé par secretKeyRef ou envFrom, jamais écrite en clair dans le Deployment7
La base de formation tourne dans un StatefulSet avec un volume persistant9, 10
La migration du schéma s'est exécutée avec succès dans un Job10
Le Deployment signalements a deux répliques disponibles, sur deux nœuds différents5, 8
Chaque conteneur de l'application a une sonde de disponibilité et une sonde de vie, des demandes de processeur et de mémoire, et une limite de mémoire4, 8
L'application tourne sans privilèges : runAsNonRoot, système de fichiers racine en lecture seule, aucune élévation de privilèges, toutes les capacités retirées, profil seccomp RuntimeDefault11
Le Service signalements a deux adresses prêtes dans ses EndpointSlices6
Un PodDisruptionBudget garde au moins une réplique disponible5, 11
L'application est exposée par une HTTPRoute ou un Ingress11
Une purge nocturne tourne en CronJob, à l'heure de Paris, sans exécutions concurrentes10
Depuis un pod du cluster, GET /sante répond 200 par le Service6, 12

Vérifier

Enregistrez le script sous verifier-lab.sh et lancez-le avec le kubeconfig du laboratoire (KUBECONFIG=... bash verifier-lab.sh). Il ne modifie rien, sauf un pod de test éphémère qu'il crée et supprime pour le dernier contrôle.

#!/usr/bin/env bash
# Vérifie les propriétés attendues du lab « Signalements sur Kubernetes ».
# Usage : KUBECONFIG=<kubeconfig du laboratoire> bash verifier-lab.sh   (exige kubectl et jq)
# Chaque contrôle affiche OK ou ÉCHEC ; le code de sortie est le nombre d'échecs.
set -uo pipefail
NS=signalements
echecs=0

controle() {   # controle "libellé" commande...
  local libelle=$1; shift
  if "$@" > /dev/null 2>&1; then
    echo "OK     $libelle"
  else
    echo "ÉCHEC  $libelle"
    echecs=$((echecs + 1))
  fi
}

k() { kubectl -n "$NS" "$@"; }

controle "le namespace signalements existe" kubectl get namespace "$NS"

dep=$(k get deployment signalements -o json 2>/dev/null)
controle "le Deployment signalements existe" test -n "$dep"
controle "deux répliques disponibles" \
  bash -c 'jq -e ".status.availableReplicas >= 2" <<< "$1"' _ "$dep"
controle "DATABASE_URL vient d'un Secret, jamais d'une valeur en clair" \
  bash -c 'jq -e "[.spec.template.spec.containers[] | (.env // [])[] | select(.name == \"DATABASE_URL\") | select(.value != null)] | length == 0" <<< "$1" &&
           jq -e "[.spec.template.spec.containers[] | ((.env // [])[] | select(.name == \"DATABASE_URL\" and .valueFrom.secretKeyRef != null)), ((.envFrom // [])[] | select(.secretRef != null))] | length > 0" <<< "$1"' _ "$dep"
controle "chaque conteneur a des sondes readiness et liveness" \
  bash -c 'jq -e "[.spec.template.spec.containers[] | .readinessProbe != null and .livenessProbe != null] | all" <<< "$1"' _ "$dep"
controle "chaque conteneur a des demandes CPU et mémoire et une limite mémoire" \
  bash -c 'jq -e "[.spec.template.spec.containers[] | .resources.requests.cpu != null and .resources.requests.memory != null and .resources.limits.memory != null] | all" <<< "$1"' _ "$dep"
controle "l'application tourne sans privilèges" \
  bash -c 'jq -e ".spec.template.spec as \$p | [\$p.containers[] | (.securityContext // {}) as \$c |
             ((\$c.runAsNonRoot // \$p.securityContext.runAsNonRoot // false) == true)
             and \$c.readOnlyRootFilesystem == true
             and \$c.allowPrivilegeEscalation == false
             and ((\$c.capabilities.drop // []) | index(\"ALL\") != null)
             and ((\$c.seccompProfile.type // \$p.securityContext.seccompProfile.type // \"\") == \"RuntimeDefault\")] | all" <<< "$1"' _ "$dep"

controle "les répliques sont sur deux nœuds différents" \
  bash -c 'sel=$(jq -r ".spec.selector.matchLabels | to_entries | map(\"\(.key)=\(.value)\") | join(\",\")" <<< "$1");
           [ "$(kubectl -n signalements get pods -l "$sel" --field-selector=status.phase=Running -o json | jq "[.items[].spec.nodeName] | unique | length")" -ge 2 ]' _ "$dep"

controle "la base de formation est un StatefulSet avec un volume persistant" \
  bash -c 'kubectl -n signalements get statefulsets -o json | jq -e "[.items[] | select((.spec.volumeClaimTemplates // []) | length > 0)] | length >= 1"'
controle "un Job de migration a réussi" \
  bash -c 'kubectl -n signalements get jobs -o json | jq -e "[.items[] | select(.status.succeeded >= 1)] | length >= 1"'

controle "le Service signalements a deux adresses prêtes" \
  bash -c '[ "$(kubectl -n signalements get endpointslices -l kubernetes.io/service-name=signalements -o json | jq "[.items[].endpoints[]? | select(.conditions.ready == true)] | length")" -ge 2 ]'
controle "un PodDisruptionBudget garde au moins une réplique" \
  bash -c 'kubectl -n signalements get pdb -o json | jq -e "[.items[] | select((.spec.minAvailable // 0) != 0 or .spec.maxUnavailable != null)] | length >= 1"'
controle "l'application est exposée par une HTTPRoute ou un Ingress" \
  bash -c 'kubectl -n signalements get httproutes -o name 2>/dev/null | grep -q . || kubectl -n signalements get ingress -o name | grep -q .'
controle "la purge est un CronJob à l'heure de Paris, sans exécutions concurrentes" \
  bash -c 'kubectl -n signalements get cronjobs -o json | jq -e "[.items[] | select(.spec.timeZone == \"Europe/Paris\" and .spec.concurrencyPolicy == \"Forbid\")] | length >= 1"'

controle "GET /sante répond 200 par le Service depuis le cluster" \
  kubectl -n "$NS" run verif-sante --rm -i --restart=Never --quiet \
    --image=curlimages/curl:8.16.0 --command -- \
    curl -fsS --max-time 5 -o /dev/null http://signalements/sante

echo
echo "$echecs échec(s)"
exit "$echecs"

Quelques contrôles demandent une explication :

  • Le contrôle des privilèges accepte runAsNonRoot et le profil seccomp au niveau du conteneur ou du pod (securityContext du pod), puisque les deux sont valides ; les autres réglages n'existent qu'au niveau du conteneur.
  • Les deux nœuds se comptent sur les pods en marche qui portent les étiquettes du sélecteur du Deployment.
  • Le dernier contrôle lance un pod jetable qui appelle le Service par son nom court : il vérifie à la fois le DNS du cluster, le Service, les EndpointSlices et la sonde de l'application. Le Service expose le port 80, comme à la leçon 11 ; adaptez l'URL si vous avez choisi un autre port.

Démonter

kubectl delete namespace signalements supprime tout ce qui y vit ; les volumes persistants provisionnés dynamiquement suivent leur politique de récupération (Delete par défaut avec kind). kind delete cluster --name formation supprime le cluster.

Plan du cours