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.
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.
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
- Équipe multi-agent auto-réparatrice dans LangGraph : Architecte, Testeur, Débogueur — Créez une machine à états LangGraph en Python où un Architecte, un Développeur, un environnement de test pytest, un Débogueur et un Réviseur collaborent et itèrent jusqu’à ce que les tests réussissent.
- Construction d’un agent ReAct dans LangChain : Exécutants, carnets de notes et leurs limites — Créez un agent ReAct utilisant des outils, observez comment l’agent et l’AgentExecutor divisent le travail, et découvrez pourquoi LangGraph gère mieux le flux de contrôle, les boucles et l’état.