Auto-remédiation & analyse des causes racines¶
Objectifs pédagogiques¶
À la fin de ce TP, vous devez être capables de :
- écrire une matrice d'alertes qui nomme le composant défaillant et la cause principale
- router les alertes par cause dans Alertmanager et les relier à un runbook
- faire annuler automatiquement une release défectueuse par Argo Rollouts
- démontrer la correction automatique d'une dérive de configuration par ArgoCD
- joindre un diagnostic automatique (événements, logs, analyse k8sgpt) aux notifications
- mesurer le temps de rétablissement de bout en bout, étape par étape
Prérequis¶
- Le TP « GameDay SecurePay avec Chaos Mesh » est réalisé : ses expériences E2 (perte de nœud), E5 (OOMKill) et E6 (HTTP 500) sont réutilisées ici
kube-prometheus-stackest installé dansmonitoring(releasekube-prometheus-stack) et Alertmanager notifie au moins un canal (Discord, Slack ou e-mail)- L'API est déployée par ArgoCD depuis le dépôt
securepay-platform(chartcharts/securepay-api/), avec un HPA - Vous avez les droits d'écriture sur les dépôts
securepay-apietsecurepay-platform kubectl,helm, le pluginkubectl argo rollouts, la CLIk8sgptetk6sont installés
Labels, ports et métriques
Les exemples supposent le label app.kubernetes.io/name: securepay-api, le port 3000 et une métrique http_requests_total portant un label code. Adaptez ces noms à votre chart et à votre instrumentation.
Consignes¶
- Toute modification durable passe par une Merge Request sur
securepay-platform: aucune correction ne se fait uniquement aveckubectl - Le remédiateur n'a jamais de droits à l'échelle du cluster, ni d'accès aux
secrets - Notez l'heure de chaque injection : elle sert à mesurer le temps de rétablissement
Contexte¶
Lors des derniers incidents de SecurePay, une heure a été perdue à comprendre qu'un dépassement mémoire provoquait des redémarrages en boucle, et une release défectueuse est restée 40 minutes en production faute de retour arrière automatique.
Votre mission : faire en sorte que la plateforme identifie seule le composant défaillant et la cause principale de chaque panne simulée, remédie automatiquement quand c'est sûr, et prouve le temps de rétablissement obtenu.
Atelier 1 : La matrice des causes racines¶
Contexte¶
« Le pod redémarre » est un symptôme. Une alerte utile dit quel composant est touché, pourquoi, et quoi faire.
Travail demandé¶
- Créez
observability/rules/securepay-root-causes.yaml, synchronisé par ArgoCD
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: securepay-root-causes
namespace: monitoring
labels:
release: kube-prometheus-stack
spec:
groups:
- name: securepay.root-causes
rules:
- alert: SecurePayContainerOOMKilled
expr: |
increase(kube_pod_container_status_restarts_total{namespace="securepay"}[10m]) > 0
and on (namespace, pod, container)
kube_pod_container_status_last_terminated_reason{namespace="securepay", reason="OOMKilled"} == 1
labels:
severity: warning
component: "{{ $labels.container }}"
cause: OOMKilled
annotations:
summary: "{{ $labels.pod }} redémarré après dépassement de sa limite mémoire"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/oomkilled.md"
- alert: SecurePayCrashLoop
expr: kube_pod_container_status_waiting_reason{namespace="securepay", reason="CrashLoopBackOff"} == 1
for: 2m
labels:
severity: warning
component: "{{ $labels.container }}"
cause: CrashLoopBackOff
annotations:
summary: "{{ $labels.pod }} redémarre en boucle"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/crashloop.md"
- alert: SecurePayImagePullFailure
expr: kube_pod_container_status_waiting_reason{namespace="securepay", reason=~"ImagePullBackOff|ErrImagePull"} == 1
for: 2m
labels:
severity: warning
component: "{{ $labels.container }}"
cause: ImagePullBackOff
annotations:
summary: "Image introuvable ou accès au registre refusé pour {{ $labels.pod }}"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/image-pull.md"
- alert: SecurePayPodNotReady
expr: kube_pod_status_ready{namespace="securepay", condition="false"} == 1
for: 10m
labels:
severity: warning
component: securepay-api
cause: PodNotReady
annotations:
summary: "{{ $labels.pod }} n'est plus prêt depuis 10 minutes"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/pod-not-ready.md"
- alert: ClusterNodeNotReady
expr: kube_node_status_condition{condition="Ready", status="true"} == 0
for: 1m
labels:
severity: critical
component: "{{ $labels.node }}"
cause: NodeNotReady
annotations:
summary: "Le nœud {{ $labels.node }} ne répond plus"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/node-not-ready.md"
- alert: SecurePayHPAAtMax
expr: |
kube_horizontalpodautoscaler_status_current_replicas{namespace="securepay"}
>= kube_horizontalpodautoscaler_spec_max_replicas{namespace="securepay"}
for: 5m
labels:
severity: warning
component: "{{ $labels.horizontalpodautoscaler }}"
cause: HPAMaxReplicas
annotations:
summary: "Le HPA {{ $labels.horizontalpodautoscaler }} est à son maximum depuis 5 minutes"
runbook_url: "https://gitlab.com/<groupe>/securepay-platform/-/blob/main/docs/runbooks/capacite.md"
Sélection des règles
Par défaut, le Prometheus de kube-prometheus-stack ne charge que les PrometheusRule portant le label release: kube-prometheus-stack, sauf si prometheus.prometheusSpec.ruleSelectorNilUsesHelmValues vaut false. Une règle absente de l'interface Prometheus (/rules) vient presque toujours de là.
- Complétez la matrice avec l'alerte de symptôme : reprenez votre alerte de burn rate du SLO et ajoutez-lui
component: securepay-apiet unrunbook_url. À défaut, partez de cette expression (fenêtres 5 min et 1 h, facteur 14,4 pour un SLO à 99,5 %) :
(
sum(rate(http_requests_total{namespace="securepay", code=~"5.."}[5m]))
/ sum(rate(http_requests_total{namespace="securepay"}[5m])) > (14.4 * 0.005)
)
and
(
sum(rate(http_requests_total{namespace="securepay", code=~"5.."}[1h]))
/ sum(rate(http_requests_total{namespace="securepay"}[1h])) > (14.4 * 0.005)
)
- Routez les alertes par cause et enrichissez les notifications dans les valeurs Helm de
kube-prometheus-stack
alertmanager:
config:
route:
receiver: equipe-securepay
group_by: [alertname, namespace, component]
routes:
- matchers:
- cause=~"OOMKilled|CrashLoopBackOff|PodNotReady|ImagePullBackOff"
receiver: diagnostic-automatique
continue: true
receivers:
- name: equipe-securepay
discord_configs:
- webhook_url_file: /etc/alertmanager/secrets/alertmanager-discord/webhook-url
title: '[{{ .Status | toUpper }}] {{ .CommonLabels.alertname }}'
message: |
Composant : {{ .CommonLabels.component }}
Cause : {{ .CommonLabels.cause }}
{{ range .Alerts }}- {{ .Annotations.summary }}
{{ end }}Runbook : {{ .CommonAnnotations.runbook_url }}
- name: diagnostic-automatique
webhook_configs:
- url: http://remediator.securepay.svc:8080/alert
send_resolved: false
Le webhook stocké dans le Secret alertmanager-discord est monté via alertmanager.alertmanagerSpec.secrets. Le récepteur diagnostic-automatique sera opérationnel à l'atelier 4.
-
Rédigez un runbook par alerte dans
docs/runbooks/: symptôme, vérifications de diagnostic (commandes et requêtes), actions de remédiation, critère de retour à la normale. La structure est détaillée dans le deck 05 de ce cours (runbooks, postmortems et ADR). -
Rejouez les expériences E5 (OOMKill) et E2 (perte de nœud) du GameDay et vérifiez le contenu des notifications reçues
Ce que vous devez observer¶
- les six règles apparaissent dans l'interface Prometheus, à l'état
inactiveen temps normal - pendant E5,
SecurePayContainerOOMKilledse déclenche aveccomponentetcause: OOMKilled - pendant E2,
ClusterNodeNotReadynomme le nœud arrêté - chaque notification contient un lien de runbook qui fonctionne
Critère de réussite de l'atelier 1¶
L'atelier est réussi si, pour E5 et E2, la notification reçue nomme le composant et la cause sans qu'aucun humain n'ait eu à investiguer, et renvoie vers un runbook existant.
Atelier 2 : Rollback automatique d'une release défectueuse¶
Contexte¶
Une release qui renvoie des erreurs 500 doit être interrompue avant d'atteindre tous les utilisateurs, sans attendre qu'un humain s'en aperçoive. Argo Rollouts expose la nouvelle version à une fraction du trafic et l'annule si l'analyse des métriques échoue.
Travail demandé¶
-
Installez le contrôleur Argo Rollouts (chart
argo/argo-rollouts, namespaceargo-rollouts, version épinglée et vérifiée) par une Application ArgoCD, puis le pluginkubectl argo rolloutssur votre poste -
Ajoutez à l'API un mécanisme d'injection de fautes, désactivé par défaut, dans le dépôt
securepay-api
// src/app.js : après le middleware de métriques, avant le montage des routes /api
const faultErrorRate = Number(process.env.FAULT_ERROR_RATE || 0);
app.use('/api', (req, res, next) => {
if (faultErrorRate > 0 && Math.random() < faultErrorRate) {
return res.status(500).json({ error: 'Injected fault' });
}
return next();
});
Ordre des middlewares
Placez ce middleware après celui qui alimente http_requests_total : sinon, les erreurs injectées n'apparaîtront pas dans les métriques et l'analyse ne verra rien.
- Dans le chart, remplacez le
Deploymentpar unRolloutcanary et ajoutez l'AnalysisTemplate
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: securepay-api
namespace: securepay
spec:
selector:
matchLabels:
app.kubernetes.io/name: securepay-api
template:
metadata:
labels:
app.kubernetes.io/name: securepay-api
spec:
containers:
- name: securepay-api
image: "<registre>/securepay-api@sha256:<digest>"
env:
- name: FAULT_ERROR_RATE
value: "0"
strategy:
canary:
steps:
- setWeight: 25
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause: { duration: 2m }
- setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: securepay
spec:
metrics:
- name: success-rate
interval: 30s
count: 6
failureLimit: 1
successCondition: len(result) > 0 && result[0] >= 0.995
provider:
prometheus:
address: http://kube-prometheus-stack-prometheus.monitoring.svc:9090
query: |
sum(rate(http_requests_total{namespace="securepay", code!~"5.."}[1m]))
/
sum(rate(http_requests_total{namespace="securepay"}[1m]))
Le pod template reprend intégralement celui de votre Deployment (probes, securityContext, ressources) : seul un extrait est montré ici. Pensez à :
- faire pointer le
scaleTargetRefdu HPA versapiVersion: argoproj.io/v1alpha1,kind: Rollout - retirer
spec.replicasdu manifest ou l'ignorer côté ArgoCD (ignoreDifferencessur/spec/replicas), sinon ArgoCD et le HPA se disputent le nombre de réplicas - autoriser par NetworkPolicy le contrôleur Argo Rollouts à joindre Prometheus si
monitoringest en refus par défaut
Comportement sans données
len(result) > 0 && result[0] >= 0.995 est volontairement fail-closed : si Prometheus ne renvoie rien ou renvoie NaN, la mesure échoue. L'analyse a donc besoin de trafic : gardez la charge k6 nominale active pendant toute la livraison.
- Livrez d'abord une release saine (par exemple un changement de variable de log) et suivez le déroulé complet sous charge k6
kubectl argo rollouts get rollout securepay-api -n securepay --watch
kubectl get analysisrun -n securepay
-
Livrez ensuite une release défectueuse par Merge Request sur
securepay-platform:FAULT_ERROR_RATE: "0.5". Notez l'heure de fusion, puis observez la synchronisation ArgoCD, le passage à 25 %, l'échec de l'AnalysisRunet l'abort -
Revenez à un état désiré cohérent : ouvrez une Merge Request de revert et vérifiez que le Rollout redevient
Healthy
Ce que vous devez observer¶
- avec la release saine, l'analyse réussit et le Rollout atteint 100 %
- avec la release défectueuse, environ un quart des pods sert des erreurs sur la moitié des requêtes : le taux de succès global tombe autour de 87 %, sous le seuil de 99,5 %
- le Rollout passe à l'état
Degraded, le canary est retiré et 100 % du trafic revient sur la version stable, sans intervention humaine - tant que le revert n'est pas fusionné, l'Application ArgoCD reste
Degraded: Git décrit encore la version défectueuse
Critère de réussite de l'atelier 2¶
L'atelier est réussi si la release défectueuse est interrompue automatiquement, si le délai entre la fusion et l'abort est mesuré, et si le revert est tracé dans l'historique Git.
Atelier 3 : Dérive de configuration corrigée par ArgoCD¶
Contexte¶
Une modification manuelle en production, même bien intentionnée, contourne le contrôle des changements. Avec selfHeal, ArgoCD détecte l'écart avec Git et le corrige.
Travail demandé¶
- Vérifiez la politique de synchronisation de l'Application
securepay-api
spec:
syncPolicy:
automated:
prune: true
selfHeal: true
ignoreDifferences:
- group: argoproj.io
kind: Rollout
jsonPointers:
- /spec/replicas
- Dérive n°1 : supprimez une NetworkPolicy du namespace et chronométrez sa réapparition
date +%H:%M:%S && kubectl delete networkpolicy <nom> -n securepay
kubectl get networkpolicy -n securepay -w
- Dérive n°2 : modifiez à la main un champ porté par Git, par exemple le maximum du HPA
kubectl patch hpa securepay-api -n securepay --type merge -p '{"spec":{"maxReplicas":20}}'
kubectl get hpa securepay-api -n securepay -w
- Consultez l'historique de l'Application dans l'interface ArgoCD : chaque correction apparaît comme une synchronisation automatique
Ce que vous devez observer¶
- les deux dérives sont annulées en quelques secondes, sans intervention
- le nombre de réplicas piloté par le HPA, lui, n'est pas écrasé grâce à
ignoreDifferences
Critère de réussite de l'atelier 3¶
L'atelier est réussi si les deux dérives sont corrigées automatiquement et si les durées de correction sont consignées comme preuves.
Atelier 4 : Diagnostic automatique joint à la notification¶
Contexte¶
La notification nomme désormais la cause. Pour réduire encore le temps de diagnostic, elle doit aussi apporter les éléments que l'astreinte irait chercher à la main : événements du pod, derniers logs avant redémarrage, analyse de k8sgpt.
Travail demandé¶
Choisissez une des deux options et justifiez votre choix dans un ADR (docs/adr/).
Déployez Robusta dans sa version open source et connectez-le à Alertmanager. Pour les alertes Prometheus courantes, Robusta joint automatiquement les logs et les événements du pod concerné à la notification. Vérifiez dans la documentation de la version retenue la configuration de l'intégration Alertmanager et du canal de sortie.
Déployez un petit service qui reçoit le webhook d'Alertmanager, collecte le diagnostic et le publie. Point de départ :
"""Remédiateur SecurePay en mode diagnostic : reçoit les alertes d'Alertmanager et publie un diagnostic."""
import json
import os
import subprocess
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
NAMESPACE = os.environ.get("TARGET_NAMESPACE", "securepay")
FENCE = "`" * 3 # bloc de code Discord
with open("/etc/remediator/discord-webhook", encoding="utf-8") as f:
DISCORD_WEBHOOK = f.read().strip()
def run(cmd):
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return (result.stdout or result.stderr).strip()[-600:]
def diagnose(labels):
pod = labels.get("pod", "")
parts = [
f"**{labels.get('alertname')}** | composant `{labels.get('component', '?')}`"
f" | cause `{labels.get('cause', '?')}` | pod `{pod or '-'}`"
]
if pod:
events = run(["kubectl", "get", "events", "-n", NAMESPACE,
"--field-selector", f"involvedObject.name={pod}"])
logs = run(["kubectl", "logs", pod, "-n", NAMESPACE, "--previous", "--tail=20"])
parts += [f"Événements :\n{FENCE}{events}{FENCE}", f"Derniers logs :\n{FENCE}{logs}{FENCE}"]
analysis = run(["k8sgpt", "analyze", "--namespace", NAMESPACE, "--filter", "Pod"])
parts.append(f"k8sgpt :\n{FENCE}{analysis}{FENCE}")
return "\n".join(parts)
def post(message):
body = json.dumps({"content": message[:1900]}).encode()
request = urllib.request.Request(
DISCORD_WEBHOOK, data=body,
headers={"Content-Type": "application/json", "User-Agent": "securepay-remediator/1.0"},
)
urllib.request.urlopen(request, timeout=10)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
payload = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
for alert in payload.get("alerts", []):
if alert.get("status") == "firing":
post(diagnose(alert.get("labels", {})))
self.send_response(200)
self.end_headers()
if __name__ == "__main__":
HTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
Il vous reste à :
- construire une image non-root embarquant Python,
kubectletk8sgpten versions épinglées, et la faire passer par votre pipeline (scan, signature) - déployer le service
remediatordanssecurepayavec un ServiceAccount dédié,automountServiceAccountToken: true(exception documentée si vos politiques l'interdisent par défaut) - monter l'URL du webhook Discord depuis un Secret chiffré dans Git
- ouvrir les flux nécessaires par NetworkPolicy : entrée depuis Alertmanager (
monitoring), sortie vers l'API Kubernetes, le DNS et Discord - vérifier que
k8sgpt analyzefonctionne depuis le pod avec la configuration in-cluster, sans option--explain: aucune IA n'est sollicitée
Dans les deux cas, le compte de service ne reçoit que des droits de lecture limités au namespace :
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: remediator
namespace: securepay
rules:
- apiGroups: [""]
resources: ["pods", "pods/log", "events"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["replicasets"]
verbs: ["get", "list"]
Vérifiez les droits effectifs :
Ce que vous devez observer¶
- pendant E5, la notification contient la raison
OOMKilledet les dernières lignes de logs du conteneur tué - le ServiceAccount ne peut ni lire les
secrets, ni agir hors desecurepay
Critère de réussite de l'atelier 4¶
L'atelier est réussi si une notification reçue pendant une panne simulée permet de comprendre la cause sans ouvrir de terminal, et si les droits du composant de diagnostic sont prouvés minimaux.
Atelier 5 : La chaîne complète et le temps de rétablissement¶
Contexte¶
Il reste à prouver la chaîne de bout en bout : panne → détection → cause identifiée → remédiation → retour au SLO, avec un temps mesuré à chaque étape.
Travail demandé¶
-
Jouez deux scénarios sous charge k6 nominale :
-
S1 - OOMKill : expérience E5 du GameDay (
StressChaosmémoire) -
S2 - Release défectueuse : Merge Request
FAULT_ERROR_RATE: "0.5"de l'atelier 2 -
Pour chaque scénario, ajoutez une annotation Grafana à l'instant de l'injection et remplissez la chronologie
| Étape | S1 - OOMKill | S2 - Release défectueuse |
|---|---|---|
| T0 - Injection | ||
| T1 - Alerte reçue | ||
| T2 - Cause identifiée | ||
| T3 - Remédiation exécutée | ||
| T4 - SLI revenus dans le SLO | ||
| Temps de rétablissement (T4 - T0) |
-
Pour S2, comparez avec une remédiation manuelle : désactivez temporairement l'étape d'analyse, rejouez la release défectueuse et mesurez le temps qu'il vous faut pour la détecter et la défaire à la main
-
Consignez les deux chronologies et leurs preuves dans
docs/resilience.mdetdocs/preuves/, puis rédigez le postmortem blameless de S2 dansdocs/postmortems/
Ce que vous devez observer¶
- en S1, la remédiation est immédiate (redémarrage par le kubelet) mais la cause, une limite mal dimensionnée, demeure : seul le postmortem la corrige durablement
- en S2, l'écart entre remédiation automatique et manuelle se mesure en minutes
Critère de réussite de l'atelier 5¶
L'atelier est réussi si les deux chronologies sont complètes, horodatées et appuyées par des preuves, et si le postmortem identifie au moins une action corrective durable.
Bonus : remédiation déclenchée par alerte avec action bornée¶
Étendez le remédiateur (option B) ou utilisez Argo Events et Argo Workflows pour qu'une alerte déclenche une action corrective :
- Déclencheur :
SecurePayPodNotReady, un pod resté non prêt 10 minutes alors que les autres réplicas servent le trafic - Action : suppression du pod concerné, recréé par son ReplicaSet. Cette action est compatible avec GitOps, car elle ne modifie aucune spec
- Bornes : au plus une action par pod toutes les 15 minutes et trois actions par heure au total ; aucune action si moins de deux pods sont prêts
- Traçabilité : chaque action est journalisée et notifiée, avec le pod, l'alerte et l'heure
- Interrupteur : une variable
REMEDIATION_ENABLEDpermet de désactiver l'action sans redéployer - Droits : ajoutez uniquement le verbe
deletesurpodsau Role du remédiateur
Pour simuler la panne, rendez la readinessProbe d'un seul pod défaillante, par exemple avec une expérience HTTPChaos ciblant le chemin de la probe de readiness. Démontrez que l'action s'exécute une fois, puis qu'elle est bien bloquée par les bornes lorsque la panne persiste.
Questions de synthèse¶
- Pourquoi une alerte « le pod redémarre » ne suffit-elle pas, et que changent les labels
componentetcause? - Pourquoi l'abort d'Argo Rollouts doit-il être suivi d'un revert dans Git ?
- Quelles remédiations automatiques sont compatibles avec
selfHeal, et lesquelles seraient annulées aussitôt ? - Quels risques un remédiateur mal conçu fait-il courir à la production, et quels garde-fous les réduisent ?
- Dans vos chronologies, quelle étape (détection, diagnostic, remédiation, vérification) pèse le plus lourd, et que faudrait-il automatiser ensuite ?