Aller au contenu

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-stack est installé dans monitoring (release kube-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 (chart charts/securepay-api/), avec un HPA
  • Vous avez les droits d'écriture sur les dépôts securepay-api et securepay-platform
  • kubectl, helm, le plugin kubectl argo rollouts, la CLI k8sgpt et k6 sont 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 avec kubectl
  • 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é

  1. 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à.

  1. Complétez la matrice avec l'alerte de symptôme : reprenez votre alerte de burn rate du SLO et ajoutez-lui component: securepay-api et un runbook_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)
)
  1. 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.

  1. 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).

  2. 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 inactive en temps normal
  • pendant E5, SecurePayContainerOOMKilled se déclenche avec component et cause: OOMKilled
  • pendant E2, ClusterNodeNotReady nomme 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é

  1. Installez le contrôleur Argo Rollouts (chart argo/argo-rollouts, namespace argo-rollouts, version épinglée et vérifiée) par une Application ArgoCD, puis le plugin kubectl argo rollouts sur votre poste

  2. 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.

  1. Dans le chart, remplacez le Deployment par un Rollout canary 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 scaleTargetRef du HPA vers apiVersion: argoproj.io/v1alpha1, kind: Rollout
  • retirer spec.replicas du manifest ou l'ignorer côté ArgoCD (ignoreDifferences sur /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 monitoring est 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.

  1. 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
  1. 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'AnalysisRun et l'abort

  2. 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é

  1. 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
  1. 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
  1. 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
  1. 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, kubectl et k8sgpt en versions épinglées, et la faire passer par votre pipeline (scan, signature)
  • déployer le service remediator dans securepay avec 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 analyze fonctionne 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 :

kubectl auth can-i --list --as=system:serviceaccount:securepay:remediator -n securepay

Ce que vous devez observer

  • pendant E5, la notification contient la raison OOMKilled et les dernières lignes de logs du conteneur tué
  • le ServiceAccount ne peut ni lire les secrets, ni agir hors de securepay

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é

  1. Jouez deux scénarios sous charge k6 nominale :

  2. S1 - OOMKill : expérience E5 du GameDay (StressChaos mémoire)

  3. S2 - Release défectueuse : Merge Request FAULT_ERROR_RATE: "0.5" de l'atelier 2

  4. 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)
  1. 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

  2. Consignez les deux chronologies et leurs preuves dans docs/resilience.md et docs/preuves/, puis rédigez le postmortem blameless de S2 dans docs/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_ENABLED permet de désactiver l'action sans redéployer
  • Droits : ajoutez uniquement le verbe delete sur pods au 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 component et cause ?
  • 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 ?