Lab : Signalements sur Kubernetes
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. kubectl1.36 etjq.
Les exigences
| Exigence | Leçon |
|---|---|
Tout vit dans le namespace signalements | 3 |
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 Deployment | 7 |
| La base de formation tourne dans un StatefulSet avec un volume persistant | 9, 10 |
| La migration du schéma s'est exécutée avec succès dans un Job | 10 |
Le Deployment signalements a deux répliques disponibles, sur deux nœuds différents | 5, 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émoire | 4, 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 RuntimeDefault | 11 |
Le Service signalements a deux adresses prêtes dans ses EndpointSlices | 6 |
| Un PodDisruptionBudget garde au moins une réplique disponible | 5, 11 |
| L'application est exposée par une HTTPRoute ou un Ingress | 11 |
| Une purge nocturne tourne en CronJob, à l'heure de Paris, sans exécutions concurrentes | 10 |
Depuis un pod du cluster, GET /sante répond 200 par le Service | 6, 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
runAsNonRootet le profil seccomp au niveau du conteneur ou du pod (securityContextdu 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.