Accueil / Articles

Le Kubernetes auto-réparateur en action : trois pannes, les signaux de SigNoz et les correctifs approuvés.

Kubernetes auto-réparateur avec OpenTelemetry et SigNoz : une boucle de panne, un arrêt en raison de mémoire insuffisante et une augmentation soudaine des tokens sur Kind, ce que montre chaque signal, ainsi que les correctifs basés sur l’IA approuvés.

Éditeur : REACTAPP.TOP11 min de lecture2499 mots

Sources: Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 7)

La démonstration comprend plusieurs composants en mouvement. Un cluster Kind local héberge SigNoz pour les traces et les journaux, ainsi qu’une application Python dont les trois points d’entrée sont délibérément défectueux. Un agent de correction surveille les anomalies et sollicite Groq pour obtenir un diagnostic, tandis qu’un tableau de bord React permet à un opérateur d’approuver ou de rejeter la correction proposée avant que quoi que ce soit ne soit exécuté. Une fois tout cela mis en place, l’aspect intéressant consiste à observer son comportement. Cette démonstration déclenche chaque panne avec l’ensemble du stack en fonctionnement, examine ce que SigNoz enregistre pour chaque incident et liste ce qui doit être modifié avant que quiconque ne puisse faire confiance à cette configuration en dehors d’un ordinateur portable.

Vérifier que le stack est opérationnel

Cinq éléments doivent être en cours d’exécution avant que toute panne ne soit déclenchée. La première vérification consiste à s’assurer que chaque pod est en bon état dans les deux espaces de noms.

# All pods should show Running
kubectl get pods -n observability
kubectl get pods -n apps

Les redirections de port pour SigNoz, le service de synthèse et le tableau de bord nécessitent chacun leur propre onglet de terminal, et la dernière commande du bloc suit les journaux de l’agent.

# Port-forwards (run each in its own tab if not already open)
# Tab A: kubectl port-forward -n observability svc/signoz 8080:8080
# Tab B: kubectl port-forward -n apps svc/summarizer 8000:80
# Tab C: kubectl port-forward -n apps svc/approval-dashboard 9000:9000
# Tab D: kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000# Confirm the agent is polling
kubectl logs -n apps deployment/remediation-agent --tail=5

Les lignes « all-clear » récentes dans cette sortie indiquent que l’agent est en bon état. Un pod qui n’est pas en état « Running » doit d’abord être examiné à l’aide de kubectl describe puis réparé. Gardez deux onglets du navigateur visibles : SigNoz sur le port 8080 et le tableau de bord sur le port 3000.

Scénario un : CrashLoopBackOff

En définissant une variable d’environnement KILL_ME sur le déploiement du summarizer puis en sollicitant son point de fin de fonctionnement, on fait en sorte que le processus s’arrête au milieu de la demande.

kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash

L’appel curl peut ne rien renvoyer ou planter, car le processus s’arrête pendant son exécution. Une surveillance des pods montre que le compteur de redémarrages augmente et que l’état passe alternativement de « Running » à « CrashLoopBackOff » ; deux ou trois cycles suffisent déjà à ce phénomène.

kubectl get pods -n apps -w

Dans SigNoz. L’aperçu du service de résumé montre que le taux d’erreurs monte à 100 % dès que le pod commence à planter, tandis que le taux de requêtes tombe à zéro chaque fois que le pod est hors service entre deux redémarrages. Un flux de trafic, puis rien, puis à nouveau du trafic, constitue la signature visuelle d’un cycle de plantage. Un déploiement se présente différemment, avec un changement de configuration propre et sans pic d’erreurs, et une partition réseau se manifeste également différemment, avec une augmentation du taux d’erreurs alors que le pod reste en état « Running ».

Le onglet « Logs », filtré sur le résumé, contient la ligne critique émise par l’application juste avant sa panne.

CRITICAL summarizer — KILL_ME is true, exiting process

Cet journal contient l’ID de suivi de la requête qui l’a déclenché, de sorte qu’un clic permet de passer de la ligne du journal au suivi, puis à la requête HTTP exacte qui a mis fin au processus.

L’agent réagit. Dès 30 secondes après le premier plantage, le cycle de sondage est activé. Il détecte un taux d’erreurs de 1,0 par rapport à un seuil de 0,10, agrège le signal et appelle Groq. Les journaux de l’agent montrent cette séquence.

kubectl logs -n apps deployment/remediation-agent --tail=20 -f
WARNING  agent — crash loop signal: error_rate=1.00
INFO     agent — anomaly detected: crash_loop — calling Groq for diagnosis
INFO     agent — diagnosis: type=crash_loop severity=critical confidence=0.91
           fix=kubectl set env deployment/summarizer -n apps KILL_ME-
INFO     incident_poster — incident posted: id=<uuid> type=crash_loop

Approbation. Le tableau de bord affiche la carte en environ dix secondes, et la correction proposée est la bonne, ce qui pousse l’opérateur à cliquer sur « Approver ». Le système backend vérifie que la commande commence par « kubectl set env » et ne contient aucun terme interdit, puis il la met en exécution. La carte est mise à jour pour confirmer l’approbation et le changement d’environnement.

✓ Approved by operator
deployment.apps/summarizer env updated

Dans le moniteur de la capsule, le démarrage suivant réussit, la boucle d’erreur s’arrête et le taux d’erreur de SigNoz revient à zéro. Avant le scénario suivant, une vérification rapide confirme que la variable a disparu.

# The approve action already removed KILL_ME, but confirm:
kubectl get deployment summarizer -n apps -o jsonpath='{.spec.template.spec.containers[0].env}' | jq

KILL_ME ne doit pas apparaître dans la sortie.

Scénario deux : OOMKill

Une boucle de requêtes POST vers l’endpoint de fuite fait augmenter progressivement la taille d’un conteneur en mémoire.

for i in {1..30}; do
  curl -s -X POST http://localhost:8000/leak | jq -c '{leaked_mb,chunks}'
  sleep 0.5
done

La mémoire augmente constamment dans le pod, et vers le 25e appel, le pod est arrêté au milieu d’une boucle, après quoi curl commence à retourner des erreurs de connexion.

Dans SigNoz. Cette fois, le taux d’erreurs ne monte pas de manière linéaire. L’arrêt dû à un manque de mémoire est brutal, et l’application ne retourne jamais de réponse d’erreur. Au contraire, le taux de requêtes chute brusquement : trafic, arrêt complet, période d’interruption, puis réapparition du trafic une fois que le pod redémarre.

Les journaux contiennent l’histoire réelle. Une fois filtrés par l’outil de synthèse, on constate que cette période d’interruption est précédée d’avertissements indiquant une augmentation de la taille du buffer.

WARNING summarizer — leak endpoint hit, bucket now holds 10 MB
WARNING summarizer — leak endpoint hit, bucket now holds 20 MB
...
WARNING summarizer — leak endpoint hit, bucket now holds 240 MB

After the last warning comes silence, with no exception, no stack trace and no graceful shutdown message, because the kernel's OOM killer gives the process no chance to log. A run of climbing warnings followed by nothing is the pattern worth recognizing.

The cluster confirms the cause from its side.

kubectl describe pod -n apps -l app=summarizer | grep -A 5 "Last State"
Last State:     Terminated
  Reason:       OOMKilled
  Exit Code:    137

Le code de sortie 137 correspond à la signature du noyau : SIGKILL (signal 9) plus 128. Il permet de distinguer une extinction due à un manque de mémoire d’une panne d’application, qui se termine avec le code 1, ainsi que d’une échec de la vérification de disponibilité, qui donne également 137 mais avec une chaîne de raison différente dans les informations de description.

L’agent réagit. Sa vérification de manque de mémoire recherche dans l’index des journaux de SigNoz la chaîne OOMKilled au cours des dix dernières minutes. En cas de correspondance, il regroupe des échantillons de journaux ainsi que l’événement de redémarrage du pod en un rapport rapide, et le diagnostic est fourni.

type=oom_kill severity=critical confidence=0.88
fix=kubectl set resources deployment/summarizer -n apps \
  --containers=summarizer --limits=memory=512Mi

La correction proposée double la limite de mémoire. Il s’agit d’une mesure à court terme raisonnable, car elle permet de gagner du temps pour publier la correction de l’application. Une fois approuvée, le backend applique une limite de 512 Mo, Kubernetes effectue un redémarrage progressif et le pod fonctionne désormais avec plus de marge.

La fuite elle-même reste présente dans le code. Ce changement ne fait que relever ce plafond ; en cas d’incident réel, il faudrait corriger le code afin de vider périodiquement le « bucket de fuites ». La tâche de l’agent est d’arrêter l’hémorragie, tandis que la cause racine reste une tâche à effectuer manuellement.

Scénario trois : latence et pic de tokens

Une série courte d’appels vers l’endpoint coûteux provoque le troisième échec.

for i in {1..5}; do
  curl -s -X POST http://localhost:8000/expensive \
    | jq '{output_tokens, length_chars}'
  sleep 2
done

Rien ne plante et rien n’est interrompu, et le pod reste en cours d’exécution. Cependant, les réponses prennent de 8 à 15 secondes chacune, et le nombre de tokens approche du plafond maximal de 2 048.

Au sein de SigNoz. C’est face à cet échec silencieux que la pile d’outils d’observabilité trouve son utilité. La courbe de latence p95, stable autour de 1,5 seconde lors d’un trafic de synthèse normal, augmente lorsque des appels coûteux sont effectués ; sa forme correspond à un changement brusque plutôt qu’à une évolution lente, car chaque appel atteint le plafond. En triant les traces par durée, ces appels apparaissent en tête avec une durée de 8 à 15 secondes chacun.

L’ouverture d’un suivi lent dans le graphique de flamme montre presque tout le temps consacré à une seule étape enfant, à savoir la requête HTTP sortante vers api.groq.com. L’étape correspondant au gestionnaire d’endpoint lui-même est courte, car le gestionnaire FastAPI ne fait presque rien, et c’est le temps de transmission avec le LLM qui domine. La longueur du contenu de réponse de cette étape est bien supérieure à celle d’une demande de synthèse normale, ce qui correspond au nombre de tokens de sortie affiché en octets sur le réseau. Ligne plate, hausse, puis retour à la ligne plate lorsque la boucle se termine : telle est la caractéristique d’un endpoint mal configuré, et non d’une panne d’infrastructure.

L’agent réagit. La vérification de la latence compare les valeurs p95 par route. La route la plus coûteuse affiche environ 11 000 ms, contre un seuil de 5 000 ms, et l’agent regroupe ces données détaillées.

{
  "check": "latency_spike",
  "spikes": { "/expensive": 11240.0 },
  "p95_ms_by_route": {
    "/summarize": 1820.0,
    "/expensive": 11240.0,
    "/healthz": 4.0
  }
}

Groq le lit et établit un diagnostic précis.

type=token_spike severity=high confidence=0.85
root_cause: The /expensive endpoint has no reasonable max_tokens budget
            and a prompt that invites long outputs, causing p95 latency
            to spike to 11s versus 1.8s on the normal /summarize path.
fix=kubectl set env deployment/summarizer -n apps MAX_TOKENS_EXPENSIVE=256

La correction proposée définit une variable d’environnement que l’application peut lire afin de limiter le budget de tokens pour cet endpoint. Après l’approbation, cette variable est mise en place. Une déploiement réel modifierait également l’application pour qu’elle litte la valeur MAX_TOKENS_EXPENSIVE depuis l’environnement. L’agent propose une modification de configuration plutôt qu’un changement de code, ce qui correspond au bon niveau d’intervention pour une correction au niveau kubectl.

Trois échecs sur une même période

Lorsque les trois scénarios sont exécutés séquentiellement, l’aperçu fourni sur une fenêtre de deux heures montre trois signatures distinctes.

  • Le cycle de plantage se manifeste par des pics et des creux dans le taux d’erreurs, atteignant 100 %, chutant à zéro puis remontant, avec des variations correspondantes dans le débit de traitement.
  • La fermeture du processus en raison d’un manque de mémoire se traduit par une chute brutale du taux de requêtes, sans pic d’erreurs, car le processus n’a jamais eu l’occasion de renvoyer une erreur.
  • L’augmentation soudaine du nombre de tokens correspond à une hausse nette de la latence p95, tandis que le taux d’erreurs reste stable, car le pod n’a jamais échoué et n’a simplement commencé à ralentir et à consommer plus de ressources.

Trois modes de défaillance présentant trois formes reconnaissables apparaissent sur un même tableau de bord, sans règles d’alerte personnalisées ni analyse manuelle des journaux. C’est là l’avantage des traces, métriques et journaux corrélés.

Changements nécessaires pour la production

La démonstration fonctionne sur un ordinateur portable et omet des aspects importants ailleurs.

Remplacer le stockage en mémoire des incidents. La liste des incidents est un dictionnaire Python situé à l’intérieur du pod backend, de sorte qu’un redémarrage efface toutes les informations. Utiliser SQLite via SQLAlchemy ne nécessite qu’un changement simple et ne requiert aucun service supplémentaire ; Redis convient lorsque le tableau de bord doit être étendu sur plusieurs répliques backend. La structure de l’API reste identique, seuls le stockage qui la soutient change.

Ajoutez des notifications Slack. Actuellement, quelqu’un doit surveiller le tableau de bord pour savoir qu’une carte est apparue. Une appel webhook vers Slack intégré dans le récepteur d’incidents du backend résout ce problème.

import httpx
SLACK_WEBHOOK = os.environ.get("SLACK_WEBHOOK_URL")def notify_slack(card: dict):
    if not SLACK_WEBHOOK:
        return
    d = card["diagnosis"]
    httpx.post(SLACK_WEBHOOK, json={
        "text": (
            f"*{d['severity'].upper()} — {d['incident_type']}* on `{card['service']}`\n"
            f"Root cause: {d['root_cause']}\n"
            f"Proposed fix: `{d['proposed_fix']}`\n"
            f"Confidence: {int(d['confidence']*100)}%\n"
            f"<http://your-dashboard-url|Review on dashboard>"
        )
    })

Appeler le notificateur à la fin du processus de réception signifie que chaque nouvel incident envoie une notification sur le canal de surveillance, et l’approbateur n’a plus besoin de surveiller une fenêtre de navigateur.

Gérez correctement les identifiants du cluster. Le backend fait appel à kubectl, ce qui est acceptable lorsque l’environnement du nœud le configure automatiquement. Dans un cluster réel, le pod backend nécessite un ServiceAccount doté des permissions RBAC appropriées, ainsi que le client Python de Kubernetes configuré pour utiliser les identifiants du cluster.

from kubernetes import client, config
config.load_incluster_config()
apps_v1 = client.AppsV1Api()
core_v1 = client.CoreV1Api()

Chaque type de commande approuvé devient alors une appel direct à l’SDK et non une chaîne de shell. Cela est plus verbeux, mais cela permet un traitement structuré des erreurs, un enregistrement des audits sur le serveur API et élimine tout risque d’injection de shell.

Raffermir la liste des autorisations. La démonstration accepte n’importe quelle commande kubectl set env. En environnement de production, il convient de la restreindre aux déploiements et aux espaces de noms nommés.

ALLOWED_TARGETS = {
    "deployment/summarizer -n apps",
    "deployment/worker -n apps",
}
def validate_command(cmd: str) -> bool:
    if not any(cmd.startswith(p) for p in ALLOWED_PREFIXES):
        return False
    if any(term in cmd for term in BLOCKED_TERMS):
        return False
    if not any(target in cmd for target in ALLOWED_TARGETS):
        return False
    return True

Un modèle ne doit pas pouvoir appliquer de correction à aucun espace de noms ou déploiement en dehors de la liste qui lui a été fournie.

Projets dignes d’étude ensuite

La démonstration a été développée de zéro pour montrer comment les différents éléments s’interconnectent, tandis qu’une utilisation en production s’appuierait sur des projets existants.

  • KubeStellar est utile lorsque les charges de travail couvrent plusieurs clusters. Il gère les tâches liées au plan de contrôle qui compliquent la correction à l’échelle des clusters, par exemple en identifiant dans quel cluster se trouve un incident avant toute suggestion de correction.
  • OpenTelemetry offre des fonctionnalités bien plus avancées que ce dont avait besoin la démonstration. Le collecteur dispose de processeurs et d’exportateurs capables d’ajouter automatiquement des métadonnées Kubernetes aux enregistrements, de sorte que les attributs des ressources n’ont pas besoin d’être ajoutés manuellement dans les fichiers de configuration des applications.
  • KEDA gère la catégorie d’ajustement de l’échelle : cette solution corrige le fait que l’agent propose parfois des ajustements qui ne devraient pas être approuvés un par un par un humain. Si l’ajustement consiste toujours à augmenter l’échelle lorsque la profondeur de la file dépasse un certain seuil, cela relève d’un KEDA ScaledObject et non d’une approbation humaine.
  • Robusta et HolmesGPT sont des versions adaptées à un environnement de production, dérivées de ce qui a été développé ici. Après avoir vu la couche de requêtes, la structure des prompts ainsi que le mécanisme d’approbation, il devient facile de comprendre leur code.
  • Ce que contient l’ensemble du système

    Le système final est un cluster Kind à trois nœuds doté d’une topologie de planification multi-nœuds réaliste, avec SigNoz déployé via Helm qui traite les traces, les métriques et les journaux grâce au OpenTelemetry Collector. Un service Python FastAPI est automatiquement instrumenté au démarrage, de sorte que chaque requête HTTP et chaque appel à un LLM devient une trace, et chaque ligne de journal est corréalisée sans code d’instrumentation dans l’application.

    Trois points d’extrémité défectueux provoquent les trois pannes les plus fréquentes dans Kubernetes : une panne de processus, une épuisement de la mémoire, ainsi qu’une détérioration des coûts et de la latence. Un agent de correction consulte SigNoz toutes les 30 secondes, applique des vérifications de seuils, regroupe les signaux en une demande structurée, demande à un LLM un diagnostic et publie une fiche d’incident. Un backend FastAPI reçoit ces fiches, valide les correctifs proposés à l’aide d’une liste d’autorisation et exécute les commandes approuvées via kubectl. Le tableau de bord React intègre des contrôles d’approbation et de rejet, un badge indiquant la confiance du modèle, un bandeau pour les diagnostics incertains, ainsi qu’un journal de chaque action effectuée.

    Tout fonctionne localement grâce à des API de niveau gratuit et ne coûte rien. Le code est suffisamment compact, avec environ 600 lignes de Python, une page HTML et quelques fichiers YAML, afin que chaque composant puisse être compris du début à la fin.

    Lectures complémentaires

  • Un tableau de bord d’approbation pour un agent de correction Kubernetes : FastAPI et React — Créez un backend FastAPI avec une liste d’autorisation kubectl ainsi qu’un tableau de bord d’approbation React, afin qu’un agent de correction basé sur un LLM ne puisse résoudre les incidents qu’après que quelqu’un ait cliqué sur « Approver ».