Accueil / Articles / Kubernetes auto-réparateur : intégration d’OpenTelemetry, SigNoz et d’une solution basée sur Groq

Kubernetes auto-réparateur : intégration d’OpenTelemetry, SigNoz et d’une solution basée sur Groq

Guide pratique pour l’utilisation de Kubernetes auto-réparateur : intégration d’OpenTelemetry, SigNoz et de solutions basées sur Groq, ainsi que contrats, vérifications et emplacements de code prêts à l’emploi pour les équipes qui adoptent ce modèle.

2248 mots

Utilisez ceci comme une version révisée destinée aux opérateurs des idées présentées dans « Self-Healing Kubernetes: Wiring OpenTelemetry, SigNoz, and a Groq-Powered Remediation Agent (PART 6) » : étapes claires, emplacements de code ordonnés, ainsi que des notes de récupération qui survivent au transfert de responsabilités. L’étape « Aperçu » fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement exemplaire, un cas d’échec et la note de rollback avant d’élargir le périmètre. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives de répétition, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure.

cd ~/k8s-aiops-blog
mkdir -p dashboard/backend dashboard/frontend
cd ~/k8s-aiops-blog/dashboard/backend
cat > main.py <<'EOF'
"""
Approval dashboard backend.
Receives incident cards from the remediation agent, serves them to the
React frontend, and applies approved kubectl commands via the Kubernetes
Python client.
"""
import logging
import shlex
import subprocess
import time
from typing import Literal
from uuid import uuid4from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModellogging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(name)s — %(message)s",
)
log = logging.getLogger("dashboard")app = FastAPI(title="Approval Dashboard")app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)# ---------------------------------------------------------------------------
# In-memory store. Fine for a demo; swap for SQLite or Redis in production.
# ---------------------------------------------------------------------------
INCIDENTS: dict[str, dict] = {}# ---------------------------------------------------------------------------
# kubectl command allowlist.
# The LLM proposes a fix. We only execute it if it matches one of these
# safe prefixes. Nothing with delete, drain, or --all gets through.
# ---------------------------------------------------------------------------
ALLOWED_PREFIXES = (
    "kubectl set env",
    "kubectl rollout restart",
    "kubectl scale",
    "kubectl set resources",
)BLOCKED_TERMS = ("delete", "drain", "--all", "cluster-admin", "exec", "cp")
# ---------------------------------------------------------------------------
# Models
# ---------------------------------------------------------------------------
class IncidentCard(BaseModel):
    id: str
    timestamp: int
    service: str
    signals: dict
    diagnosis: dict
    status: Literal["pending", "approved", "rejected"] = "pending"
class ActionRequest(BaseModel):
    actor: str = "operator"
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/healthz")
def healthz():
    return {"ok": True}
@app.post("/api/incidents", status_code=201)
def receive_incident(card: IncidentCard):
    INCIDENTS[card.id] = card.dict()
    log.info("incident received: id=%s type=%s", card.id, card.diagnosis.get("incident_type"))
    return {"id": card.id}
@app.get("/api/incidents")
def list_incidents():
    # Most recent first
    return sorted(INCIDENTS.values(), key=lambda c: c["timestamp"], reverse=True)
@app.get("/api/incidents/{incident_id}")
def get_incident(incident_id: str):
    if incident_id not in INCIDENTS:
        raise HTTPException(status_code=404, detail="not found")
    return INCIDENTS[incident_id]
@app.post("/api/incidents/{incident_id}/approve")
def approve_incident(incident_id: str, req: ActionRequest):
    card = INCIDENTS.get(incident_id)
    if not card:
        raise HTTPException(status_code=404, detail="not found")
    if card["status"] != "pending":
        raise HTTPException(status_code=409, detail=f"incident already {card['status']}")    proposed = card["diagnosis"].get("proposed_fix", "")
    log.info("approve requested: id=%s fix=%r actor=%s", incident_id, proposed, req.actor)    # Validate against allowlist
    allowed = any(proposed.startswith(p) for p in ALLOWED_PREFIXES)
    blocked = any(term in proposed for term in BLOCKED_TERMS)    if not allowed or blocked:
        log.error("REJECTED by allowlist: %r", proposed)
        raise HTTPException(
            status_code=400,
            detail=f"proposed command did not pass allowlist validation: {proposed!r}",
        )    # Apply the fix
    try:
        args = shlex.split(proposed)
        result = subprocess.run(
            args,
            capture_output=True,
            text=True,
            timeout=30,
        )
        if result.returncode != 0:
            log.error("kubectl failed: %s", result.stderr)
            raise HTTPException(status_code=500, detail=f"kubectl error: {result.stderr}")        log.info("kubectl succeeded: %s", result.stdout.strip())
        card["status"] = "approved"
        card["applied_at"] = int(time.time())
        card["applied_by"] = req.actor
        card["kubectl_output"] = result.stdout.strip()
        INCIDENTS[incident_id] = card
        return {"status": "approved", "kubectl_output": result.stdout.strip()}    except subprocess.TimeoutExpired:
        raise HTTPException(status_code=504, detail="kubectl timed out")
@app.post("/api/incidents/{incident_id}/reject")
def reject_incident(incident_id: str, req: ActionRequest):
    card = INCIDENTS.get(incident_id)
    if not card:
        raise HTTPException(status_code=404, detail="not found")
    if card["status"] != "pending":
        raise HTTPException(status_code=409, detail=f"incident already {card['status']}")    card["status"] = "rejected"
    card["rejected_at"] = int(time.time())
    card["rejected_by"] = req.actor
    INCIDENTS[incident_id] = card
    log.info("incident rejected: id=%s actor=%s", incident_id, req.actor)
    return {"status": "rejected"}
EOF
cat > requirements.txt <<'EOF'
fastapi==0.115.0
uvicorn[standard]==0.32.0
pydantic==2.9.2
httpx==0.27.2
EOF
cat > Dockerfile <<'EOF'
FROM python:3.11-slim
# Install kubectl so the backend can apply fixes
RUN apt-get update && apt-get install -y curl && \
    curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" && \
    install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl && \
    rm kubectl && \
    apt-get cleanWORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .EXPOSE 9000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "9000"]
EOF
cd ~/k8s-aiops-blog/dashboard/frontend
cat > index.html <<'EOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>SRE Approval Dashboard</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/7.23.2/babel.min.js"></script>
  <link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"/>
</head>
<body class="bg-gray-950 text-gray-100 min-h-screen">
<div id="root"></div><script type="text/babel">
const API = "http://localhost:9000";const SEVERITY_COLOR = {
  critical: "bg-red-600",
  high:     "bg-orange-500",
  medium:   "bg-yellow-500",
  low:      "bg-blue-500",
};const TYPE_LABEL = {
  crash_loop:    "Crash Loop",
  oom_kill:      "OOM Kill",
  latency_spike: "Latency Spike",
  token_spike:   "Token Spike",
  unknown:       "Unknown",
};function ConfidenceBadge({ value }) {
  const pct = Math.round((value || 0) * 100);
  const color = pct >= 80 ? "text-green-400" : pct >= 50 ? "text-yellow-400" : "text-red-400";
  return (
    <span className={`text-xs font-mono font-bold ${color}`}>
      {pct}% confidence
    </span>
  );
}function IncidentCard({ card, onAction }) {
  const d = card.diagnosis || {};
  const sev = d.severity || "low";
  const isPending = card.status === "pending";  return (
    <div className="bg-gray-900 border border-gray-700 rounded-xl p-5 mb-4 shadow-lg">
      {/* Header row */}
      <div className="flex items-center justify-between mb-3">
        <div className="flex items-center gap-2">
          <span className={`text-xs font-bold px-2 py-0.5 rounded-full text-white ${SEVERITY_COLOR[sev] || "bg-gray-600"}`}>
            {sev.toUpperCase()}
          </span>
          <span className="font-semibold text-white">
            {TYPE_LABEL[d.incident_type] || d.incident_type}
          </span>
          <span className="text-gray-400 text-sm">— {card.service}</span>
        </div>
        <div className="flex items-center gap-3">
          <ConfidenceBadge value={d.confidence} />
          <span className="text-xs text-gray-500">
            {new Date(card.timestamp * 1000).toLocaleTimeString()}
          </span>
        </div>
      </div>      {/* Root cause */}
      <p className="text-sm text-gray-300 mb-3">
        <span className="text-gray-500 mr-1">Root cause:</span>
        {d.root_cause}
      </p>      {/* Proposed fix */}
      <div className="bg-gray-800 rounded-lg px-4 py-2 mb-4 font-mono text-sm text-green-300 flex items-center gap-2">
        <span className="text-gray-500 select-none">lt;/span>
        {d.proposed_fix}
      </div>      {/* Low confidence warning */}
      {(d.confidence || 0) < 0.6 && isPending && (
        <div className="bg-yellow-900 border border-yellow-600 rounded-lg px-3 py-2 mb-3 text-yellow-300 text-xs">
          ⚠ Low confidence — review signals carefully before approving.
        </div>
      )}      {/* Action buttons or status badge */}
      {isPending ? (
        <div className="flex gap-3 mt-2">
          <button
            onClick={() => onAction(card.id, "approve")}
            className="bg-green-600 hover:bg-green-500 text-white text-sm font-semibold px-5 py-2 rounded-lg transition"
          >
            ✓ Approve
          </button>
          <button
            onClick={() => onAction(card.id, "reject")}
            className="bg-red-700 hover:bg-red-600 text-white text-sm font-semibold px-5 py-2 rounded-lg transition"
          >
            ✗ Reject
          </button>
        </div>
      ) : (
        <div className={`inline-block text-xs font-bold px-3 py-1 rounded-full mt-1 ${
          card.status === "approved" ? "bg-green-800 text-green-300" : "bg-red-900 text-red-300"
        }`}>
          {card.status === "approved" ? "✓ Approved" : "✗ Rejected"}
          {card.applied_by && ` by ${card.applied_by}`}
        </div>
      )}      {/* kubectl output on approved cards */}
      {card.kubectl_output && (
        <div className="mt-3 bg-gray-800 rounded-lg px-4 py-2 font-mono text-xs text-gray-400">
          {card.kubectl_output}
        </div>
      )}
    </div>
  );
}function App() {
  const [incidents, setIncidents] = React.useState([]);
  const [loading, setLoading] = React.useState(true);
  const [error, setError] = React.useState(null);
  const [acting, setActing] = React.useState(null);  const fetchIncidents = () => {
    fetch(`${API}/api/incidents`)
      .then(r => r.json())
      .then(data => { setIncidents(data); setLoading(false); setError(null); })
      .catch(e => { setError(e.message); setLoading(false); });
  };  React.useEffect(() => {
    fetchIncidents();
    const id = setInterval(fetchIncidents, 10000); // poll every 10s
    return () => clearInterval(id);
  }, []);  const handleAction = async (id, action) => {
    setActing(id);
    try {
      const r = await fetch(`${API}/api/incidents/${id}/${action}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ actor: "operator" }),
      });
      if (!r.ok) {
        const err = await r.json();
        alert(`Action failed: ${err.detail}`);
      }
      fetchIncidents();
    } catch (e) {
      alert(`Network error: ${e.message}`);
    } finally {
      setActing(null);
    }
  };  const pending   = incidents.filter(c => c.status === "pending");
  const resolved  = incidents.filter(c => c.status !== "pending");  return (
    <div className="max-w-3xl mx-auto px-4 py-8">
      {/* Header */}
      <div className="mb-8">
        <h1 className="text-2xl font-bold text-white">SRE Approval Dashboard</h1>
        <p className="text-gray-400 text-sm mt-1">
          Human-in-the-loop remediation — review every fix before it runs
        </p>
      </div>      {loading && <p className="text-gray-500">Loading incidents…</p>}
      {error   && <p className="text-red-400">Could not reach backend: {error}</p>}      {/* Pending */}
      {pending.length > 0 && (
        <section className="mb-8">
          <h2 className="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">
            Awaiting Review ({pending.length})
          </h2>
          {pending.map(c => (
            <IncidentCard key={c.id} card={c} onAction={handleAction} />
          ))}
        </section>
      )}      {pending.length === 0 && !loading && (
        <div className="bg-gray-900 border border-gray-700 rounded-xl p-8 text-center text-gray-500 mb-8">
          No pending incidents. The cluster looks healthy.
        </div>
      )}      {/* Resolved */}
      {resolved.length > 0 && (
        <section>
          <h2 className="text-sm font-semibold text-gray-400 uppercase tracking-widest mb-3">
            Resolved ({resolved.length})
          </h2>
          {resolved.map(c => (
            <IncidentCard key={c.id} card={c} onAction={handleAction} />
          ))}
        </section>
      )}
    </div>
  );
}ReactDOM.createRoot(document.getElementById("root")).render(<App />);
</script>
</body>
</html>
EOF
cat > Dockerfile <<'EOF'
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
EOF
cd ~/k8s-aiops-blog/dashboard/backend
docker build -t approval-backend:v1 .
kind load docker-image approval-backend:v1 --name aiops
cd ~/k8s-aiops-blog/dashboard/frontend
docker build -t approval-frontend:v1 .
kind load docker-image approval-frontend:v1 --name aiops
cat > ~/k8s-aiops-blog/k8s/dashboard.yaml <<'EOF'
# ── Backend ────────────────────────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
  name: approval-backend
  namespace: apps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: approval-backend
  template:
    metadata:
      labels:
        app: approval-backend
    spec:
      containers:
        - name: backend
          image: approval-backend:v1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 9000
          readinessProbe:
            httpGet:
              path: /healthz
              port: 9000
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests:
              memory: "128Mi"
              cpu: "100m"
            limits:
              memory: "256Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: approval-dashboard
  namespace: apps
spec:
  selector:
    app: approval-backend
  ports:
    - port: 9000
      targetPort: 9000
---
# ── Frontend ───────────────────────────────────────────────────────────────
apiVersion: apps/v1
kind: Deployment
metadata:
  name: approval-frontend
  namespace: apps
spec:
  replicas: 1
  selector:
    matchLabels:
      app: approval-frontend
  template:
    metadata:
      labels:
        app: approval-frontend
    spec:
      containers:
        - name: frontend
          image: approval-frontend:v1
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          resources:
            requests:
              memory: "64Mi"
              cpu: "50m"
            limits:
              memory: "128Mi"
              cpu: "200m"
---
apiVersion: v1
kind: Service
metadata:
  name: approval-frontend-svc
  namespace: apps
spec:
  selector:
    app: approval-frontend
  ports:
    - port: 3000
      targetPort: 80
EOF
kubectl apply -f ~/k8s-aiops-blog/k8s/dashboard.yaml
kubectl rollout status deployment/approval-backend -n apps
kubectl rollout status deployment/approval-frontend -n apps
kubectl port-forward -n apps svc/approval-dashboard 9000:9000
kubectl port-forward -n apps svc/approval-frontend-svc 3000:3000
kubectl logs -n apps deployment/remediation-agent --tail=5 -f
kubectl get pods -n apps -w
kubectl set env deployment/summarizer -n apps KILL_ME=true
curl -s http://localhost:8000/crash
... WARNING  crash loop signal: error_rate=1.00
... INFO     anomaly detected: crash_loop — calling Groq for diagnosis
... INFO     diagnosis: type=crash_loop severity=critical confidence=0.91
              fix=kubectl set env deployment/summarizer -n apps KILL_ME-
... INFO     incident posted: id=<uuid> type=crash_loop
CRITICAL  Crash Loop — summarizer               91% confidence
Root cause: The KILL_ME environment variable is set to true, causing the process to exit immediately on each start.
$ kubectl set env deployment/summarizer -n apps KILL_ME-
[ ✓ Approve ]  [ ✗ Reject ]
✓ Approved by operator
deployment.apps/summarizer env updated

Checklist opérationnelle

Lors de l’étape de la checklist opérationnelle, notez d’abord les éléments requis : données nécessaires, signal de succès et conséquences en cas d’échec partiel. Cette checklist permet de rester honnête lors des modifications ultérieures du code.

Gardez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système.

Faites un point après les étapes coûteuses. Le système de reprise ne doit pas facturer à nouveau la même appel au LLM lorsque l’opérateur réessaie un nœud ultérieur.

Versionnez les dépendances de Pin et enregistrez le digest de l’image utilisée pour exécuter la démo. La reproductibilité vaut mieux que les connaissances empiriques.

Dokumentez à la fois le parcours normal et les procédures de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations ultérieures.

Faites des points de contrôle après les étapes coûteuses. Le système de reprise ne doit pas facturer à nouveau la même appel au LLM lorsque l’opérateur tente une nouvelle fois un nœud ultérieur.

Au préalable de promouvoir l’ensemble, figez les versions, conservez une transcription exemplaire pour le parcours critique et confirmez les étapes de rollback. Les environnements partagés nécessitent des limites de débit, des vérifications d’attribution et un responsable clair pour la rotation des secrets. Préférez une fiabilité banale à des démos originales mais peu fiables.

Note de lot pour 4a22289d433c : gardez les clés du fournisseur hors du répertoire, fixez un plafond pour les tokens par session, et stockez les transcriptions à côté des fichiers d’évaluation afin que les remplacements ultérieurs de modèles restent comparables.

Pour l’étape 0 des notes de renforcement de sécurité, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du système.

Détail de renforcement 0/765 : mesurez le temps d’exécution, la classe de l’erreur et la consommation de tokens pour cette note, puis décidez s’il convient de conserver la modification en vous basant sur un ensemble fixe de critères plutôt que sur des observations subjectives.

Lors de la première étape des notes de renforcement, notez d’abord les éléments essentiels : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Préférez des unités petites et testables plutôt que des scripts complexes. Lorsqu’une étape échoue, l’erreur doit indiquer une responsabilité précise plutôt qu’un processus embrouillé.

Détail de renforcement 1/765 : mesurez le temps d’exécution, la catégorie de l’erreur et la consommation de tokens pour cette note, puis décidez si vous souhaitez conserver la modification en vous basant sur un ensemble de critères prédéfinis plutôt que sur des observations subjectives.

La deuxième étape des notes de renforcement fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Recueillez un exemple idéal de fonctionnement, un cas d’échec et une note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les surprises financières lorsque le processus passe de l’environnement de démonstration à des environnements partagés.

Détail de renforcement 2/765 : mesurer le temps d’exécution, la classe d’erreur et la consommation de tokens pour cette note, puis décider de conserver ou non le changement en se basant sur un ensemble de questions prédéfini plutôt que sur des anecdotes.

Pour la phase 3 de la note de renforcement, définir les entrées, le responsable de l’étape et les critères d’achèvement avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documenter ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie du produit, et non d’une mise en forme ultérieure.

Détail de renforcement 3/765 : mesurer le temps d’exécution, la classe d’erreur et la consommation de tokens pour cette note, puis décider de conserver ou non le changement en se basant sur un ensemble de questions prédéfini plutôt que sur des anecdotes.