lumnik. / scénarios Tous les scénarios · English

k9s dit Running mais le cluster brûle: quel pod regarder d'abord?

Objectif Demander à son cluster Kubernetes quels pods traiter en premier, et recevoir la liste triée par la règle de l'astreinte, vérifiée contre kubectl.

Un vrai cluster, cinq équipes, cinq pods cassés par Kubernetes lui-même:

Une règle Kotlin de l'astreinte fixe la priorité; le modèle traduit la question, il ne décide rien.

manifests/lab-10-broken.yaml
# payment-api: fills its memory past a 64Mi limit — the kernel kills it (OOMKilled), then CrashLoopBackOff.apiVersion: apps/v1kind: Deploymentmetadata: { name: payment-api, namespace: payments, labels: { team: team-payments } }spec:  replicas: 1  selector: { matchLabels: { app: payment-api } }  template:    metadata: { labels: { app: payment-api, team: team-payments } }    spec:      containers:        - name: api          image: busybox:1.36          command: ["sh", "-c", "sleep 20; tail /dev/zero"]          resources: { requests: { memory: 32Mi, cpu: 10m }, limits: { memory: 64Mi, cpu: 200m } }---# catalog-api: a busy loop under a 100m CPU limit — saturated and throttled, never killed.apiVersion: apps/v1kind: Deploymentmetadata: { name: catalog-api, namespace: catalog, labels: { team: team-catalog } }spec:  replicas: 1  selector: { matchLabels: { app: catalog-api } }  template:    metadata: { labels: { app: catalog-api, team: team-catalog } }    spec:      containers:        - name: api          image: busybox:1.36          command: ["sh", "-c", "while :; do :; done"]          resources: { requests: { memory: 16Mi, cpu: 50m }, limits: { memory: 64Mi, cpu: 100m } }---# checkout-worker: its readiness probe asks a path nginx does not serve — never Ready, Unhealthy events.apiVersion: apps/v1kind: StatefulSetmetadata: { name: checkout-worker, namespace: checkout, labels: { team: team-checkout } }spec:  serviceName: checkout-worker  replicas: 1  selector: { matchLabels: { app: checkout-worker } }  template:    metadata: { labels: { app: checkout-worker, team: team-checkout } }    spec:      containers:        - name: worker          image: nginx:1.27-alpine          readinessProbe: { httpGet: { path: /ready, port: 80 }, periodSeconds: 5 }          resources: { requests: { memory: 16Mi, cpu: 10m }, limits: { memory: 64Mi, cpu: 100m } }---# billing-api: its liveness probe fails — the kubelet restarts it again and again.apiVersion: apps/v1kind: Deploymentmetadata: { name: billing-api, namespace: billing, labels: { team: team-billing } }spec:  replicas: 1  selector: { matchLabels: { app: billing-api } }  template:    metadata: { labels: { app: billing-api, team: team-billing } }    spec:      containers:        - name: api          image: nginx:1.27-alpine          livenessProbe: { httpGet: { path: /health, port: 80 }, periodSeconds: 5, failureThreshold: 2 }          resources: { requests: { memory: 16Mi, cpu: 10m }, limits: { memory: 64Mi, cpu: 100m } }---# storefront: mounts a ConfigMap nobody created — stuck in ContainerCreating, FailedMount events.apiVersion: apps/v1kind: Deploymentmetadata: { name: storefront, namespace: frontend, labels: { team: team-web } }spec:  replicas: 1  selector: { matchLabels: { app: storefront } }  template:    metadata: { labels: { app: storefront, team: team-web } }    spec:      containers:        - name: web          image: nginx:1.27-alpine          volumeMounts: [{ name: config, mountPath: /etc/storefront }]          resources: { requests: { memory: 16Mi, cpu: 10m }, limits: { memory: 64Mi, cpu: 100m } }      volumes: [{ name: config, configMap: { name: storefront-config } }]
manifests/lab-30-reader.yaml
# What lumnik may do in the cluster: read pods and events, cluster-wide. Nothing else.apiVersion: v1kind: Namespacemetadata: { name: lumnik-reader }---apiVersion: v1kind: ServiceAccountmetadata: { name: lumnik-reader, namespace: lumnik-reader }---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRolemetadata: { name: lumnik-reader }rules:  - apiGroups: [""]    resources: [pods, events]    verbs: [get, list]  - apiGroups: [metrics.k8s.io]    resources: [pods]    verbs: [get, list]---apiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: { name: lumnik-reader }roleRef: { apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: lumnik-reader }subjects: [{ kind: ServiceAccount, name: lumnik-reader, namespace: lumnik-reader }]---# A long-lived token for the hub's connector secret (projected tokens expire within the hour).apiVersion: v1kind: Secretmetadata:  name: lumnik-reader-token  namespace: lumnik-reader  annotations: { kubernetes.io/service-account.name: lumnik-reader }type: kubernetes.io/service-account-token
manifests/10-cluster.yaml
# The cluster's own API, read-only: pods, what they use now, and their recent Warning events.# The token and the CA come from the lumnik-reader ServiceAccount (k8s/30-reader.yaml): lm secret set.apiVersion: connectors.lumnik.io/v1kind: Connectormetadata:  name: k8s-cluster  connector_type: rest-generic  scopes: [devops-pod-triage]spec:  base_url: https://kubernetes.default.svc  auth: { kind: bearer, token_env: K8S_READER_TOKEN }  tls: { ca_secret: CLUSTER_CA }  defaults:    incremental: { mode: full }  endpoints:    - id: pods      method: GET      path: /api/v1/pods      pagination: { kind: cursor, cursor_param: continue, cursor_path: $.metadata.continue, response_path: $.items }      target:        kind: hybrid        table: connector.k8s_pods        unique_key: $.metadata.uid        promote:          - { name: namespace,              path: "$.metadata.namespace",                                     type: text }          - { name: pod,                    path: "$.metadata.name",                                          type: text }          - { name: team,                   path: "$.metadata.labels.team",                                   type: text }          - { name: ready,                  path: "$.status.containerStatuses[0].ready",                      type: boolean }          - { name: restarts,               path: "$.status.containerStatuses[0].restartCount",               type: integer }          - { name: waiting_reason,         path: "$.status.containerStatuses[0].state.waiting.reason",       type: text }          - { name: last_terminated_reason, path: "$.status.containerStatuses[0].lastState.terminated.reason", type: text }          - { name: cpu_limit,              path: "$.spec.containers[0].resources.limits.cpu",                type: decimal, transform: parse_quantity }          - { name: memory_limit,           path: "$.spec.containers[0].resources.limits.memory",             type: decimal, transform: parse_quantity }          - { name: pod_key,       type: text }      # filled by pod-triage.kts          - { name: priority,      type: text }          - { name: priority_rank, type: integer }          - { name: signal,        type: text }      mapping:        cell:          - kotlin: { script: hooks:pod-triage.kts }    - id: usage      method: GET      path: /apis/metrics.k8s.io/v1beta1/pods      pagination: { kind: cursor, cursor_param: continue, cursor_path: $.metadata.continue, response_path: $.items }      target:        kind: hybrid        table: connector.k8s_pod_usage        unique_key: "$.concat($.metadata.namespace, $.metadata.name)"        promote:          - { name: namespace, path: "$.metadata.namespace",     type: text }          - { name: pod,       path: "$.metadata.name",          type: text }          - { name: cpu,       path: "$.containers[0].usage.cpu",    type: decimal, transform: parse_quantity }          - { name: memory,    path: "$.containers[0].usage.memory", type: decimal, transform: parse_quantity }          - { name: pod_key,   type: text }          # filled by pod-key.kts      mapping:        cell:          - kotlin: { script: hooks:pod-key.kts }    - id: warnings      method: GET      path: /api/v1/events?fieldSelector=type%3DWarning,involvedObject.kind%3DPod      pagination: { kind: cursor, cursor_param: continue, cursor_path: $.metadata.continue, response_path: $.items }      target:        kind: hybrid        table: connector.k8s_warnings        unique_key: $.metadata.uid        promote:          - { name: namespace, path: "$.involvedObject.namespace", type: text }          - { name: pod,       path: "$.involvedObject.name",      type: text }          - { name: reason,    path: "$.reason",                   type: text }          - { name: count,     path: "$.count",                    type: integer }          - { name: last_seen, path: "$.lastTimestamp",            type: timestamptz }          - { name: message,   path: "$.message",                  type: text }          - { name: pod_key,   type: text }          # filled by pod-key.kts      mapping:        cell:          - kotlin: { script: hooks:pod-key.kts }
manifests/pod-triage.kts
// The operator's triage rule, over one pod's own state (the pods API). Deterministic: the model never decides it.//   critical — the kernel killed it for memory, or it loops on restarts (CrashLoopBackOff)//   high     — it is not ready: a probe fails, or it cannot start (ContainerCreating, ImagePullBackOff…)//   medium   — it is ready but has restarted//   none     — nothing in its state// Saturation is not here: usage comes from metrics.k8s.io, another endpoint.@Suppress("UNCHECKED_CAST")val cells = bindings["cells"] as MutableMap<String, Any?>val waiting = cells["waiting_reason"] as String?val terminated = cells["last_terminated_reason"] as String?val restarts = (cells["restarts"] as Number?)?.toInt() ?: 0val ready = cells["ready"] == truecells["pod_key"] = "${cells["namespace"]}/${cells["pod"]}"val (priority, signal) = when {    terminated == "OOMKilled" -> "critical" to "OOMKilled, $restarts restarts"    waiting == "CrashLoopBackOff" -> "critical" to "CrashLoopBackOff, $restarts restarts"    !ready -> "high" to (waiting ?: "not ready")    restarts > 0 -> "medium" to "$restarts restarts"    else -> "none" to ""}cells["priority"] = prioritycells["priority_rank"] = mapOf("critical" to 1, "high" to 2, "medium" to 3, "none" to 4).getValue(priority)cells["signal"] = signal
manifests/pod-key.kts
// The key of the pod a row is about: namespace/pod — the pods, their usage and their events join on it.@Suppress("UNCHECKED_CAST")val cells = bindings["cells"] as MutableMap<String, Any?>cells["pod_key"] = "${cells["namespace"]}/${cells["pod"]}"
manifests/20-pod.yaml
kind: Entityname: KubernetesPodlabel: Kubernetes poddescription: "One pod of the cluster, read from the Kubernetes API: its state, the operator's triage priority computed from that state, and the CPU it uses now against its limit. A pod to investigate first has priority critical, then high, then medium; or uses 90% or more of its CPU limit (cpu / cpu_limit)."match: [pod_key]scopes: [devops-pod-triage]columns:  pod_key:       { label: "namespace/pod — the pod's key" }  namespace:     { label: "Kubernetes namespace" }  pod:           { label: "Pod name" }  team:          { label: "Team that owns the pod (its team label)" }  ready:         { label: "Whether the pod's container is ready" }  restarts:      { label: "How many times its container restarted" }  priority:      { label: "Triage priority from the pod's state, by the operator's rule: critical, high or medium are to investigate; none needs no investigation" }  priority_rank: { label: "Order of the priority: 1 critical, 2 high, 3 medium, 4 none — sort by it, ascending" }  signal:        { label: "What in the pod's state set its priority (empty for none)" }  cpu_limit:     { label: "CPU limit, in cores" }  memory_limit:  { label: "Memory limit, in bytes" }  cpu:           { label: "CPU used now, in cores (metrics.k8s.io); saturated when cpu is 90% or more of cpu_limit" }  memory:        { label: "Memory used now, in bytes (metrics.k8s.io)" }sources:  - table: k8s_pods    map:      pod_key: pod_key      namespace: namespace      pod: pod      team: team      ready: ready      restarts: restarts      priority: priority      priority_rank: priority_rank      signal: signal      cpu_limit: cpu_limit      memory_limit: memory_limitjoins:  - table: k8s_pod_usage    from: pod_key    to: pod_key    map:      cpu: cpu      memory: memory
manifests/30-warning.yaml
kind: Entityname: KubernetesWarninglabel: Kubernetes Warning event about a poddescription: "One Warning event the cluster recorded about a pod, read from the Kubernetes API: its reason, how many times it repeated, when it was last seen."match: [pod_key, reason]scopes: [devops-pod-triage]columns:  pod_key:   { label: "namespace/pod — the key of the pod the event is about" }  reason:    { label: "Why Kubernetes warned: Unhealthy, BackOff, FailedMount…" }  count:     { label: "How many times this event repeated" }  last_seen: { label: "When it was last seen" }  message:   { label: "Kubernetes' own message" }sources:  - table: k8s_warnings    map:      pod_key: pod_key      reason: reason      count: count      last_seen: last_seen      message: message
$ lm ask --scope devops-pod-triage --lang fr "Quels pods dois-je analyser en priorité ?"
Il y a 5 pods à analyser en priorité :

1. billing/billing-api-55d9bc55bb-vdnrr (priorité : critical)
2. payments/payment-api-5997d486df-lfskh (priorité : critical)
3. checkout/checkout-worker-0 (priorité : high)
4. frontend/storefront-5758c4cfc7-82sqr (priorité : high)
5. catalog/catalog-api-54b5c97cdf-q9v2g (priorité : none)

Total de pods à analyser : 5
  data as of: 2026-10-29T21:25:01.643821Z (last completed run)

  POD_KEY                                NAMESPACE  POD                           TEAM           READY  RESTARTS  PRIORITY  PRIORITY_RANK  SIGNAL                          CPU_LIMIT  MEMORY_LIMIT  CPU
  billing/billing-api-55d9bc55bb-vdnrr   billing    billing-api-55d9bc55bb-vdnrr  team-billing   false  197       critical  1              CrashLoopBackOff, 197 restarts  0.1        67108864
  payments/payment-api-5997d486df-lfskh  payments   payment-api-5997d486df-lfskh  team-payments  false  99        critical  1              OOMKilled, 99 restarts          0.2        67108864
  checkout/checkout-worker-0             checkout   checkout-worker-0             team-checkout  false  0         high      2              not ready                       0.1        67108864      0.000044149
  frontend/storefront-5758c4cfc7-82sqr   frontend   storefront-5758c4cfc7-82sqr   team-web       false  0         high      2              ContainerCreating               0.1        67108864
  catalog/catalog-api-54b5c97cdf-q9v2g   catalog    catalog-api-54b5c97cdf-q9v2g  team-catalog   true   0         none      4                                              0.1        67108864      0.100062579
  5 rows returned

  SQL: SELECT "pod_key", "namespace", "pod", "team", "ready", "restarts", "priority", "priority_rank", "signal", "cpu_limit", "memory_limit", "cpu"
FROM "connector"."v_kubernetespod"
WHERE "priority" IN ('critical', 'high', 'medium') OR "cpu" >= 0.9 * "cpu_limit"
ORDER BY "priority_rank"
LIMIT 500

$ lm ask --scope devops-pod-triage --lang fr -o json "Quels pods dois-je analyser en priorité ?"
{
  "answer": "Il y a 5 pods à analyser en priorité :\n\n1. billing/billing-api-55d9bc55bb-vdnrr (priorité : critical)\n2. payments/payment-api-5997d486df-lfskh (priorité : critical)\n3. checkout/checkout-worker-0 (priorité : high)\n4. frontend/storefront-5758c4cfc7-82sqr (priorité : high)\n5. catalog/catalog-api-54b5c97cdf-q9v2g (priorité : none)\n\nTotal de pods à analyser : 5",
  "sql": "SELECT \"pod_key\", \"namespace\", \"pod\", \"team\", \"ready\", \"restarts\", \"priority\", \"priority_rank\", \"signal\", \"cpu_limit\", \"memory_limit\", \"cpu\"\nFROM \"connector\".\"v_kubernetespod\"\nWHERE \"priority\" IN ('critical', 'high', 'medium') OR \"cpu\" >= 0.9 * \"cpu_limit\"\nORDER BY \"priority_rank\"\nLIMIT 500",
  "rowCount": 5,
  "columns": [
    "pod_key",
    "namespace",
    "pod",
    "team",
    "ready",
    "restarts",
    "priority",
    "priority_rank",
    "signal",
    "cpu_limit",
    "memory_limit",
    "cpu"
  ],
  "rows": [
    {
      "pod_key": "billing/billing-api-55d9bc55bb-vdnrr",
      "namespace": "billing",
      "pod": "billing-api-55d9bc55bb-vdnrr",
      "team": "team-billing",
      "ready": "false",
      "restarts": 197,
      "priority": "critical",
      "priority_rank": 1,
      "signal": "CrashLoopBackOff, 197 restarts",
      "cpu_limit": 0.1,
      "memory_limit": 67108864,
      "cpu": null
    },
    {
      "pod_key": "payments/payment-api-5997d486df-lfskh",
      "namespace": "payments",
      "pod": "payment-api-5997d486df-lfskh",
      "team": "team-payments",
      "ready": "false",
      "restarts": 99,
      "priority": "critical",
      "priority_rank": 1,
      "signal": "OOMKilled, 99 restarts",
      "cpu_limit": 0.2,
      "memory_limit": 67108864,
      "cpu": null
    },
    {
      "pod_key": "checkout/checkout-worker-0",
      "namespace": "checkout",
      "pod": "checkout-worker-0",
      "team": "team-checkout",
      "ready": "false",
      "restarts": 0,
      "priority": "high",
      "priority_rank": 2,
      "signal": "not ready",
      "cpu_limit": 0.1,
      "memory_limit": 67108864,
      "cpu": 0.000044149
    },
    {
      "pod_key": "frontend/storefront-5758c4cfc7-82sqr",
      "namespace": "frontend",
      "pod": "storefront-5758c4cfc7-82sqr",
      "team": "team-web",
      "ready": "false",
      "restarts": 0,
      "priority": "high",
      "priority_rank": 2,
      "signal": "ContainerCreating",
      "cpu_limit": 0.1,
      "memory_limit": 67108864,
      "cpu": null
    },
    {
      "pod_key": "catalog/catalog-api-54b5c97cdf-q9v2g",
      "namespace": "catalog",
      "pod": "catalog-api-54b5c97cdf-q9v2g",
      "team": "team-catalog",
      "ready": "true",
      "restarts": 0,
      "priority": "none",
      "priority_rank": 4,
      "signal": "",
      "cpu_limit": 0.1,
      "memory_limit": 67108864,
      "cpu": 0.100062579
    }
  ],
  "dataAsOf": "2026-10-29T21:25:01.643821Z",
  "otherDoors": [],
  "disputed": [],
  "disputedTruncated": false,
  "abstained": false,
  "understoodAs": null
}
$ lm ask --scope devops-pod-triage --lang fr "Quels pods utilisent 90 % ou plus de leur limite CPU ?"
La réponse rédigée citait un chiffre que ces lignes ne confirment pas ; elle est donc retenue. Les lignes et le SQL ci-dessous font foi.
  data as of: 2026-10-29T21:25:01.643821Z (last completed run)

  POD_KEY                               NAMESPACE  POD                           TEAM          CPU_LIMIT  CPU
  catalog/catalog-api-54b5c97cdf-q9v2g  catalog    catalog-api-54b5c97cdf-q9v2g  team-catalog  0.1        0.100062579
  1 row returned

  SQL: SELECT "pod_key", "namespace", "pod", "team", "cpu_limit", "cpu"
FROM "connector"."v_kubernetespod"
WHERE "cpu" >= 0.9 * "cpu_limit"
LIMIT 500

$ lm ask --scope devops-pod-triage --lang fr -o json "Quels pods utilisent 90 % ou plus de leur limite CPU ?"
{
  "answer": "La réponse rédigée citait un chiffre que ces lignes ne confirment pas ; elle est donc retenue. Les lignes et le SQL ci-dessous font foi.",
  "sql": "SELECT \"pod_key\", \"namespace\", \"pod\", \"team\", \"cpu_limit\", \"cpu\"\nFROM \"connector\".\"v_kubernetespod\"\nWHERE \"cpu\" >= 0.9 * \"cpu_limit\"\nLIMIT 500",
  "rowCount": 1,
  "columns": [
    "pod_key",
    "namespace",
    "pod",
    "team",
    "cpu_limit",
    "cpu"
  ],
  "rows": [
    {
      "pod_key": "catalog/catalog-api-54b5c97cdf-q9v2g",
      "namespace": "catalog",
      "pod": "catalog-api-54b5c97cdf-q9v2g",
      "team": "team-catalog",
      "cpu_limit": 0.1,
      "cpu": 0.100062579
    }
  ],
  "dataAsOf": "2026-10-29T21:25:01.643821Z",
  "otherDoors": [],
  "disputed": [],
  "disputedTruncated": false,
  "abstained": true,
  "understoodAs": null
}

Limites

  • Le modèle traduit la question; une autre formulation peut produire un autre SQL. Le SQL est affiché pour être relu.
  • La saturation combine deux API: c'est le SQL qui la calcule, pas le script. Une colonne calculée par le hub la rendrait déterministe.
  • Un premier conteneur par pod seulement: un pod à plusieurs conteneurs est jugé sur le premier.
  • Cluster kind de démonstration: les pannes sont réelles, le cluster est petit.

DevOps · incident

Cinq pods en vrac. Par où commencer?

L'astreinte pose la question à son cluster.

lumnik lit l'API Kubernetes, en lecture seule.

Cluster kind de démonstration

Le problème

Trois API, aucune vue d'ensemble.

  • Les pods disent leur état.
  • metrics.k8s.io dit leur consommation.
  • Les events disent ce qui alerte.

Le piège

catalog-api: Running, 1/1, aucun event.

  • Il tourne à 100% de sa limite CPU.
  • Seule metrics.k8s.io le montre.

Ce qu'il a fallu écrire

cells["priority"] = when {
  terminated == "OOMKilled" -> "critical"
  !ready -> "high"
  ...

La règle de l'astreinte, en Kotlin.

Le hub l'applique à chaque pod lu. Le scénario complet: 5 fichiers, 168 lignes.

La réponse

Cinq pods, du critique au saturé.

  • payment-api: OOMKilled, là où kubectl dit CrashLoopBackOff.
  • checkout-worker: sonde en échec.
  • catalog-api: 100% de sa limite CPU.

La preuve

Le runner recalcule la règle avec kubectl.

  • Même liste, ou le scénario échoue.
  • 8 fois sur 8, les mêmes cinq pods.

Essayer

Le scénario se rejoue sur kind.

Un connecteur, deux scripts, deux entités, deux questions: chaque étape est publiée avec sa sortie.

Ce scénario demande lumnik Pro.

Voir l'offre →