Runbook de validation EKS â fastapi-eks-project
Tests de validation pour le cluster EKS, organisĂ©s par catĂ©gorie. Principe directeur : valider par la preuve (test), pas par l'intention (le manifest). Chaque contrĂŽle de sĂ©curitĂ© ou de rĂ©silience se valide par un test positif (ce qui doit passer) ET un test nĂ©gatif (ce qui doit ĂȘtre bloquĂ©).
Table des catégories
- NetworkPolicy (enforcement réseau)
- HPA / Autoscaling
- TLS / cert-manager
- Pod Security Admission (PSA)
- Probes / Health
- Connectivité / Smoke tests
- Secrets externes (ESO / IRSA)
- GitOps / ArgoCD
- Observabilité (métriques, logs)
- Alerting (Prometheus â Alertmanager â Slack)
- BoĂźte Ă outils
1. NetworkPolicy
PrĂ©requis pour que ce soit enforced â depuis la migration Cilium (#80, ADR 019) : l'enforcement est dĂ©sormais natif Ă Cilium, qui applique les NetworkPolicy K8s standard sans aws-node ni addon managĂ©. Le piĂšge INC-046 (NetworkPolicy inertes en vpc-cni self-managed) ne s'applique plus : il n'y a plus de vpc-cni du tout.
Vérifier que Cilium enforce :
kubectl get pods -n kube-system -l k8s-app=cilium # agent Cilium Running (et 0 aws-node)
cilium status | grep -i policy # si la CLI cilium est dispo
Avant #80 (vpc-cni managé), le prérequis était
aws eks list-addonslistantvpc-cni+enableNetworkPolicy=true. ObsolĂšte sous Cilium.
Test négatif (le deny fonctionne)
Depuis un pod soumis Ă une politique egress restrictive, tenter une sortie NON autorisĂ©e. La policy fastapi autorise seulement 5432 (RDS), 443 (AWS), 53 (DNS). Le port 80 doit ĂȘtre bloquĂ©.
# L'image fastapi n'embarque pas curl : tester via python (DNS 53 autorisé résout le nom,
# le connect TCP port 80 doit ĂȘtre droppĂ©).
kubectl exec -n fastapi deploy/fastapi -- python -c "import socket; socket.create_connection(('example.com',80),5)"; echo "EXIT=$?"
OSError: [Errno 101] Network is unreachable + exit non nul.
Cilium droppe le paquet d'egress â Network unreachable (plus net que le Connection timed out du vpc-cni).
Si le connect rĂ©ussit (EXIT=0, aucune erreur) : policies non enforced â ne pas valider, vĂ©rifier l'agent Cilium.
Test positif (les allow rules passent)
Valider que le trafic autorisé fonctionne toujours. /healthz/ready fait un SELECT 1 sur RDS,
donc il exerce DNS (port 53, résolution du hostname RDS) + port 5432 (connexion TCP).
curl -s https://api.devopsyouss.com/healthz/ready
{"status":"ok"} (HTTP 200)
Si 503 : une rĂšgle allow (DNS ou 5432) bloque Ă tort.
Test inter-pods (optionnel)
Vérifier qu'un pod hors allowlist ne joint pas un pod protégé.
# pod témoin dans un autre namespace, tente de joindre le service fastapi
kubectl run probe --image=curlimages/curl -n default --restart=Never --rm -it -- \
curl -m 5 http://fastapi.fastapi.svc.cluster.local/
from) ou timeout.
2. HPA / Autoscaling
Prérequis : metrics-server installé (via bootstrap Ansible) + HPA défini (min:1 max:5, cpu:70%).
Ătape 1 â metrics-server remonte des mĂ©triques
kubectl top nodes
kubectl top pods -n fastapi
3m 60Mi). Si error: Metrics API not available, metrics-server KO.
Ătape 2 â le HPA lit les mĂ©triques
kubectl get hpa -n fastapi
cpu: 5%/70% (un pourcentage réel).
PiĂšge classique EKS : cpu: <unknown>/70% = metrics-server ne remonte pas, HPA aveugle.
Ătape 3 â dĂ©clencher le scale-up par la charge
Méthode recommandée : charge DEPUIS l'intérieur du cluster (pas de limite de bande passante internet). La charge externe depuis le homelab plafonne à ~46 req/s (latence ~1s/req), insuffisant pour saturer le CPU.
# Terminal 1 â observation
watch -n 2 kubectl get hpa,pods -n fastapi
# Terminal 2 â charge interne via fortio (namespace default, sans NetworkPolicy)
kubectl run loadgen --image=fortio/fortio --restart=Never --rm -it -n default -- \
load -c 150 -qps 0 -t 120s http://fastapi.fastapi.svc.cluster.local/
/ (JSON statique, CPU-bound) et NON /healthz/ready (I/O-bound sur la DB, ne fait pas monter le CPU)
- -qps 0 = débit max, -c 150 = 150 connexions concurrentes
Calcul HPA : cible = ramener le CPU moyen Ă 70%. Ă 194% sur 1 replica â ceil(1 Ă 194/70) = 3 replicas.
Comme la CPU limit (500m) vaut 200% de la request (250m), le CPU moyen reste > 70% â scale jusqu'Ă maxReplicas (5).
Ătape 4 â observer le scale-down
Ă l'arrĂȘt de la charge, le scale-down N'EST PAS immĂ©diat : fenĂȘtre de stabilisation par dĂ©faut de 5 min (anti-flapping). Les replicas restent hauts puis redescendent vers min (1). Normal, pas un bug.
Charge externe (alternative, avec réserve)
# apache2-utils (ab) ou hey ; limité par la bande passante homelab -> AWS
ab -t 120 -n 1000000 -c 200 https://api.devopsyouss.com/
3. TLS / cert-manager
Vérifier les ClusterIssuers
kubectl get clusterissuers
letsencrypt-staging et letsencrypt-prod, READY: True.
Vérifier le certificat (wildcard depuis #76)
Depuis #76, un seul certificat wildcard *.devopsyouss.com (cert wildcard-devopsyouss, secret wildcard-devopsyouss-tls, ns fastapi, issuer letsencrypt-prod) couvre api, grafana et tout futur sous-domaine. Le listener HTTPS du Gateway le référence.
kubectl get certificate -n fastapi
kubectl describe certificate wildcard-devopsyouss -n fastapi
READY: True, et dans les events The certificate has been successfully issued.
Le wildcard impose le challenge DNS-01 (un HTTP-01 ne peut pas valider un *), déjà en place via Cloudflare. La colonne de renouvellement confirme que cert-manager gÚre le cycle de vie (~30j avant expiration).
Vérifier la chaßne TLS cÎté client
# Voir l'émetteur et la validité
curl -vI https://api.devopsyouss.com 2>&1 | grep -Ei "issuer|subject|expire"
# Ou inspection complĂšte
echo | openssl s_client -connect api.devopsyouss.com:443 -servername api.devopsyouss.com 2>/dev/null \
| openssl x509 -noout -issuer -subject -dates
(STAGING) Let's Encrypt, NON trusté navigateur (normal).
Prod : issuer Let's Encrypt, cadenas vert.
Bascule staging -> prod
- Ăditer
certificate.yaml:issuerRef.name: letsencrypt-staging->letsencrypt-prod kubectl apply -f certificate.yamlkubectl delete secret wildcard-devopsyouss-tls -n fastapi(force la ré-émission)- Surveiller
kubectl describe certificate wildcard-devopsyouss -n fastapi
4. Pod Security Admission (PSA)
Prérequis : namespace fastapi labellisé pod-security.kubernetes.io/enforce: restricted
(posé par le bootstrap Ansible, INC-045).
Vérifier les labels du namespace
kubectl get ns fastapi --show-labels
pod-security.kubernetes.io/enforce=restricted.
Test négatif (pod non conforme rejeté)
kubectl run nginx-test --image=nginx -n fastapi
violates PodSecurity "restricted": runAsNonRoot, seccompProfile, etc.).
Si le pod est créé : PSA non enforced.
Test positif (pod conforme admis)
Le pod fastapi (runAsNonRoot, seccomp RuntimeDefault, capabilities drop ALL, readOnlyRootFilesystem)
doit ĂȘtre admis et passer Running 1/1.
kubectl get pods -n fastapi
5. Probes / Health
L'app expose deux endpoints dédiés (issue #47) :
- /healthz/live : le process répond (liveness)
- /healthz/ready : SELECT 1 sur la DB, renvoie 503 si la DB est injoignable (readiness)
# Direct sur un pod (port 8080)
kubectl exec -n fastapi deployment/fastapi -- wget -qO- http://localhost:8080/healthz/live
kubectl exec -n fastapi deployment/fastapi -- wget -qO- http://localhost:8080/healthz/ready
# Via l'URL publique
curl -s https://api.devopsyouss.com/healthz/ready
/healthz seul renvoie 404, ce n'est pas une route. Les routes sont /healthz/live et /healthz/ready.
6. Connectivité / Smoke tests
# Nodes prĂȘts
kubectl get nodes
# Tout le namespace applicatif
kubectl get all -n fastapi
# DNS interne (depuis un pod autorisé)
kubectl exec -n fastapi deployment/fastapi -- nslookup kubernetes.default
# Endpoint public de bout en bout
curl -s https://api.devopsyouss.com/ # {"message":"Hello all the World"}
curl -s https://api.devopsyouss.com/docs # Swagger UI
# Hostname du NLB (le CNAME Cloudflare est créé tout seul par ExternalDNS, #66/#70)
kubectl get svc -n envoy-gateway-system
# Attendu : un Service LoadBalancer avec un hostname *.elb.amazonaws.com, servi par un
# NLB (depuis #70 : annotation aws-load-balancer-type=nlb via l'EnvoyProxy nlb-config),
# PAS un Classic ELB. CÎté console AWS : type = network, et aucun CLB orphelin.
# Logs applicatifs
kubectl logs -n fastapi -l app=fastapi -f
7. Secrets externes (ESO / IRSA)
Principe : aucun secret en clair dans Git. External Secrets Operator (ESO) lit AWS Secrets Manager via une identité IRSA (least-privilege, ARNs scopés) et matérialise des Secrets Kubernetes. ADR 009.
Vérifier la chaßne ESO
kubectl get secretstore -A
kubectl get externalsecret -A
SecretStore : STATUS = Valid (ESO parle Ă AWS via IRSA).
- ExternalSecret : STATUS = SecretSynced, READY = True (secret récupéré et écrit).
Trois secrets gérés à ce jour :
| ExternalSecret | Namespace | Secret K8s produit | Usage |
|---|---|---|---|
| app (DB) | fastapi |
creds appli | mot de passe RDS |
grafana |
monitoring |
grafana-admin |
login admin Grafana (#76) |
alertmanager-slack |
monitoring |
alertmanager-slack |
webhook Slack (#77) |
Test : le secret existe cÎté cluster, jamais dans Git
kubectl -n monitoring get secret grafana-admin -o jsonpath='{.data.admin-user}' | base64 -d
grep -rn "webhook\|password" k8s/ # ne doit JAMAIS sortir une vraie valeur
PiÚge vécu (#77, validation live 2026-06-14)
Le stack Terraform persistent crĂ©e le secret AWS ; l'ephemeral (IRSA) le rĂ©fĂ©rence. Appliquer l'ephemeral avant le persistent fait Ă©chouer le data "aws_secretsmanager_secret" (couldn't find resource). Ordre : persistent (crĂ©e) â ephemeral (rĂ©fĂ©rence).
8. GitOps / ArgoCD
Principe : déploiement pull-based (#72, ADR 013). La CI ne fait plus kubectl apply : elle écrit le tag d'image dans k8s/base/kustomization.yaml (champ images:) et pousse sur develop ; ArgoCD réconcilie. Git = seule source de vérité. Guide : Comprendre ArgoCD.
Vérifier l'état des Applications
kubectl -n argocd get applications
SYNC STATUS = Synced, HEALTH STATUS = Healthy. Le root-app apps (app-of-apps) déploie fastapi, kube-prometheus-stack, monitoring, loki, alloy.
Test selfHeal (drift d'un champ géré)
Modifier un champ présent dans Git (ex. l'image) :
kubectl -n fastapi set image deploy/fastapi fastapi=nginx:alpine # drift volontaire
OutOfSync et remet la valeur de Git (selfHeal) ; la ressource redevient Synced.
Limite à connaßtre : ArgoCD est additif (validé en test 2026-06-14)
ArgoCD ne supprime que les champs qu'il a lui-mĂȘme posĂ©s (suivis via last-applied / managedFields). Un champ ajoutĂ© hors-bande (kubectl patch d'un champ absent du Git, ex. un command: injectĂ©) n'est pas vu comme un drift : l'Application reste Synced et le selfHeal ne le retire pas. C'est voulu (coexistence avec HPA, webhooks mutants, opĂ©rateurs). Pour retirer un tel champ : le faire Ă la main, ou forcer (Replace=true / ServerSideApply strict). RĂšgle mentale : ArgoCD dĂ©fend ce que tu as dĂ©clarĂ©, il ne police pas ce que tu n'as pas dĂ©clarĂ©.
Cascade app-of-apps (validé en test 2026-06-14)
Le root-app apps gÚre l'Application fastapi avec son propre selfHeal. Pour neutraliser temporairement le selfHeal de fastapi, museler d'abord le parent apps, sinon il répare l'enfant aussitÎt :
kubectl -n argocd patch application apps --type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
kubectl -n argocd patch application fastapi --type merge -p '{"spec":{"syncPolicy":{"automated":{"selfHeal":false}}}}'
# ... manipulation ...
# RESTAURER ensuite selfHeal:true sur les DEUX (ne pas laisser une dérive non corrigée)
spec.replicas est retiré du Git et mis en ignoreDifferences dans l'Application, sinon selfHeal se bat avec le HPA.
9. Observabilité (métriques, logs)
Stack : kube-prometheus-stack + Loki + Alloy, déployés par ArgoCD (#74/#75, ADR 014/015). Vitrine : https://grafana.devopsyouss.com (#76, login admin via secret grafana-admin). Guide : Comprendre l'observabilité.
AccÚs Grafana en local (si non exposé)
kubectl -n monitoring port-forward svc/kube-prometheus-stack-grafana 3000:80
# mot de passe admin = secret grafana-admin (clé admin-password), PAS "prom-operator"
Métriques : Prometheus scrape /metrics
Dans Grafana â Explore (datasource Prometheus) :
up{namespace="fastapi", endpoint="http"} # = 1 (cible scrapée)
http_requests_total # le compteur de requĂȘtes monte
up = 1, http_requests_total présent. Le ServiceMonitor fastapi scrape /metrics en interne (ClusterIP, jamais public, #81).
Dashboard
Dashboard FastAPI RED (Rate / Errors / Duration) provisionné as-code (sidecar Grafana), calé sur les vraies métriques (http_requests_total{status="5xx"}, histogrammes http_request_duration_*).
Logs : Loki + Alloy
Dans Grafana â Explore (datasource Loki) :
{namespace="fastapi"}
namespace / pod / container.
10. Alerting (Prometheus â Alertmanager â Slack)
Principe : 5 PrometheusRule FastAPI (#77 + #93, ADR 016) â Alertmanager (routage par sĂ©vĂ©ritĂ©) â Slack. Webhook gĂ©rĂ© par ESO (jamais en clair). RĂšgles : k8s/platform/monitoring/prometheusrules-fastapi.yaml. Guide : Comprendre l'alerting.
| Alerte | Expression (résumé) | for |
sévérité |
|---|---|---|---|
FastAPITargetDown |
up{namespace="fastapi",endpoint="http"} == 0 |
2m | critical |
FastAPITargetMissing |
absent(up{namespace="fastapi",endpoint="http"}) |
2m | critical |
FastAPIPodCrashLooping |
increase(kube_pod_container_status_restarts_total{namespace="fastapi"}[10m]) > 3 |
5m | critical |
FastAPIHighErrorRate |
part de 5xx > 5% sur 5m | 5m | warning |
FastAPIHighLatency |
p99 latence > 1s sur 5m | 5m | warning |
Test synthétique (routage Slack, sans rien casser)
kubectl -n monitoring port-forward svc/alertmanager-operated 9093:9093 &
amtool alert add FastAPITest severity=warning --alertmanager.url=http://localhost:9093
[FIRING] dans Slack puis [RESOLVED] Ă l'expiration. Le Watchdog (deadman's switch, toujours firing par design) et InfoInhibitor doivent ĂȘtre routĂ©s vers null (absents de Slack).
Test live « target down + CrashLoop » (réalisé le 2026-06-14)
Provoquer un CrashLoop (pods présents mais qui plantent) déclenche deux alertes :
# 1. couper le selfHeal (cf. section 8, cascade app-of-apps)
# 2. casser la commande + relancer des pods
kubectl -n fastapi patch deploy fastapi --type json \
-p '[{"op":"add","path":"/spec/template/spec/containers/0/command","value":["sh","-c","exit 1"]}]'
kubectl -n fastapi scale deploy fastapi --replicas=2
Normal â Pending â Firing (le for: est le dĂ©lai anti-faux-positif). Suivre la condition en direct dans Explore : increase(kube_pod_container_status_restarts_total{namespace="fastapi"}[10m]). Puis Slack reçoit [FIRING] FastAPITargetDown + [FIRING] FastAPIPodCrashLooping.
Rétablir :
kubectl -n fastapi patch deploy fastapi --type json \
-p '[{"op":"remove","path":"/spec/template/spec/containers/0/command"}]'
# RESTAURER selfHeal:true sur apps PUIS fastapi
Normal, [RESOLVED] dans Slack.
â ïž PiĂšge clĂ© : « sĂ©rie absente » vs « sĂ©rie Ă 0 »
FastAPITargetDown repose sur up == 0. Selon la panne, le comportement diffĂšre :
| Situation | Ătat de la sĂ©rie up |
up == 0 se déclenche ? |
|---|---|---|
Scale Ă 0 (--replicas=0) |
aucun pod â aucune cible â sĂ©rie absente | NON (rien Ă Ă©valuer) |
| Pod prĂ©sent mais plantĂ© (CrashLoop, scrape KO) | cible existe â sĂ©rie = 0 | OUI |
up == 0 ne couvre donc pas la disparition totale (scale-à -0, deployment supprimé). Corrigé (#93) par la rÚgle FastAPITargetMissing = absent(up{namespace="fastapi", endpoint="http"}), validée live le 2026-06-16 (test ci-dessous).
Note :
InfoInhibitorest désormais routé versnull(#94), validé le 2026-06-16 (absent de Slack pendant le test ci-dessous).
Test live « scale-Ă -0 â FastAPITargetMissing » (rĂ©alisĂ© le 2026-06-16)
Prouver que la disparition de la cible (série up absente) déclenche bien FastAPITargetMissing. Deux piÚges à connaßtre, sinon le scale-à -0 ne « tient » pas.
# 1. Museler le selfHeal, le PARENT d'abord (sinon le root-app répare fastapi)
kubectl -n argocd patch application apps --type merge -p '{"spec":{"syncPolicy":{"automated":null}}}'
kubectl -n argocd patch application fastapi --type merge -p '{"spec":{"syncPolicy":{"automated":null}}}'
# 2. Supprimer le HPA (minReplicas=2 maintiendrait 2 pods), puis scale Ă 0
kubectl -n fastapi delete hpa fastapi
kubectl -n fastapi scale deploy fastapi --replicas=0
up{namespace="fastapi"} passe en No data (la série disparaßt, elle ne tombe pas à 0). AprÚs for: 2m, Slack reçoit [FIRING] FastAPITargetMissing (critical). InfoInhibitor n'apparaßt pas (routé null, #94).
Rétablir :
kubectl -n argocd patch application fastapi --type merge -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'
kubectl -n argocd patch application apps --type merge -p '{"spec":{"syncPolicy":{"automated":{"prune":true,"selfHeal":true}}}}'
# Le HPA est recréé par ArgoCD, MAIS il ne réveille pas un workload à 0 -> scale à la main
kubectl -n fastapi scale deploy fastapi --replicas=2
up revient Ă 1, Slack [RESOLVED].
â ïž PiĂšge HPA-Ă -0 (vĂ©cu) : un HPA refuse de scaler un Deployment Ă
replicas=0(ScalingDisabled: scaling is disabled since the replica count of the target is zero). Il ne scale qu'à partir de ℠1 (sauf feature gateHPAScaleToZero). Il faut donc « réveiller » le workload à la main (scale --replicas=2), ensuite le HPA reprend le relais. Couplé au fait que/spec/replicasest enignoreDifferences(#82), ArgoCD ne remonte pas non plus les replicas tout seul : le scale manuel est obligatoire pour sortir du 0.
11. BoĂźte Ă outils
| Outil | Usage | Note |
|---|---|---|
kubectl top |
CPU/mémoire pods et nodes | nécessite metrics-server |
fortio |
charge interne (HPA) | image fortio/fortio, Ă lancer en pod dans le cluster |
ab (apache2-utils) |
charge externe / smoke HTTPS | limité par la bande passante homelab |
hey |
charge externe (alternative Ă ab) | binaire Go unique |
openssl s_client |
inspection chaĂźne TLS | issuer, dates, SAN |
curl -vI |
headers HTTP + infos TLS rapides | |
nslookup / dig |
résolution DNS | tester la rÚgle egress 53 |
aws eks list-addons |
vérifier les addons managés | clé pour l'enforcement NetworkPolicy |
amtool |
tester le routage Alertmanager â Slack | amtool alert add (firing/resolved synthĂ©tique) |
promtool check rules |
valider les PrometheusRule en local |
sans cluster, avant merge |
| Grafana Explore | requĂȘtes PromQL (mĂ©triques) / LogQL (logs) Ă la main | l'outil pour diagnostiquer en direct |
kubectl -n argocd get applications |
état Synced/Healthy des Applications | la photo GitOps en une commande |
Rappel des deux tests qui ferment la boucle sécurité
| ContrÎle | Test négatif (deny) | Test positif (allow) |
|---|---|---|
| NetworkPolicy | curl http://example.com dans le pod -> timeout |
/healthz/ready -> 200 (DNS+5432 OK) |
| PSA | kubectl run nginx dans fastapi -> rejeté |
pod fastapi -> Running 1/1 |
| HPA | (n/a) | charge fortio -> scale 1 vers 5, puis scale-down |
| TLS | cert staging non trusté navigateur | cert prod -> cadenas vert |