From 381e6ab6ab2d360e41508166afb005a07c61ac3d Mon Sep 17 00:00:00 2001 From: Cristian Felipe Cruz Buitron Date: Mon, 10 Aug 2026 00:47:23 -0500 Subject: [PATCH] docs(playbooks): documentar incidente medusa-deploy atascado en Init (2026-08) Postmortem del incidente resuelto en e9c93bf: sintoma reportado como CrashLoopBackOff que en realidad era Init:0/2 indefinido (0 restarts). Documenta las hipotesis descartadas en orden (secret/DATABASE_URL, NetworkPolicy, salud de Postgres, ResourceQuota, DNS, MTU/PMTUD cross-node), la causa raiz (pg_isready no parseaba ?sslmode=disable en la URI, exit 3 "no attempt"), el fix con flags explicitos, y la deuda tecnica pendiente en la branch fix/medusa-wait-for-postgres. --- .../incidente-medusa-crashloop-2026-08.md | 212 ++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 docs/playbooks/incidente-medusa-crashloop-2026-08.md diff --git a/docs/playbooks/incidente-medusa-crashloop-2026-08.md b/docs/playbooks/incidente-medusa-crashloop-2026-08.md new file mode 100644 index 0000000..e30c3d1 --- /dev/null +++ b/docs/playbooks/incidente-medusa-crashloop-2026-08.md @@ -0,0 +1,212 @@ +# Incidente: medusa-deploy atascado sin levantar (2026-08) + +**Ventana del incidente:** ~2026-08-08 (onset) → 2026-08-10 05:31 UTC (resuelto) +**Servicio afectado:** `medusa-deploy` en namespace `ecommerce` +**Impacto:** el rollout nuevo nunca llegaba a estar listo; el pod anterior +(`medusa-deploy-59878b956f-hf45b`) siguió sirviendo tráfico sin interrupción +durante todo el incidente (`maxUnavailable: 0`), por lo que no hubo downtime +de cara al usuario — el impacto fue "no se puede desplegar", no "el servicio +está caído". + +## Síntoma inicial reportado vs. estado real + +El incidente se reportó como **CrashLoopBackOff** ("sigue igual desde ayer, +más de 20-24 horas"). Al correr `kubectl get pods -n ecommerce -o wide` esa +descripción resultó **inexacta**: no había ningún pod en CrashLoopBackOff. +El estado real era: + +``` +commerce-postgres-0 1/1 Running 0 4d9h +medusa-deploy-59878b956f-hf45b 1/1 Running 0 21h (revisión vieja, sana) +medusa-deploy-9d8784d7b-g9jql 0/1 Init:0/2 0 3h56m (revisión nueva, atascada) +``` + +0 *restarts* en el pod nuevo — no estaba crasheando y reiniciando, estaba +**colgado indefinidamente en el primer init container** (`wait-for-postgres`), +sin timeout ni backoff que lo sacara de ese estado. Esta distinción importa: +un pod en `Init:0/2` con 0 restarts no aparece en las alertas típicas de +CrashLoopBackOff, lo que probablemente explica por qué pasó desapercibido +tanto tiempo. + +**Lección:** verificar siempre el estado real con `kubectl get pods` antes de +asumir el tipo de falla que describe quien reporta el incidente. Un +`Init:X/Y` con 0 restarts y un `CrashLoopBackOff` requieren líneas de +investigación distintas. + +## Hipótesis descartadas (en orden) + +### 1. Credenciales / secret inválido o vacío + +- `kubectl get secret -n ecommerce commerce-secrets` → la clave + `DATABASE_URL` existe. +- Se verificó longitud del valor (`~126` caracteres, no vacío ni truncado) + sin volcar el contenido en texto plano. +- Se probó el `DATABASE_URL` real contra Postgres con `pg_isready` ejecutado + **dentro del propio pod `commerce-postgres-0`**: `accepting connections`, + exit 0. +- **Descartada:** el secret existe, tiene el formato correcto y las + credenciales son válidas. + +### 2. NetworkPolicy bloqueando tráfico + +- `kubectl describe networkpolicy -n ecommerce ecommerce-network-security` + → `PodSelector: ` (aplica a todos los pods), `Allowing ingress + traffic: To Port: , From: NamespaceSelector: ` (permite todo el + ingress desde cualquier namespace), `Not affecting egress traffic`. +- **Descartada:** la policy es efectivamente allow-all para ingress y no + restringe egress. No podía estar bloqueando la conexión de `medusa` hacia + `postgres-svc`. + +### 3. Salud de Postgres + +- `kubectl exec -n ecommerce commerce-postgres-0 -- psql -U medusa -c + "SELECT count(*) FROM pg_stat_activity;"` → `8` conexiones activas, + respuesta inmediata. +- `kubectl get endpoints -n ecommerce postgres-svc` → apunta correctamente a + `10.42.0.98:5432`, la IP real del pod. +- **Descartada:** Postgres estaba sano, aceptando conexiones y con el + Endpoint del Service correctamente resuelto. + +### 4. ResourceQuota / LimitRange del namespace + +- `kubectl describe resourcequota -n ecommerce` → `pods: 7/12`, + `requests.cpu: 625m/3`, `requests.memory: 1504Mi/4Gi`, `limits.cpu: + 4250m/8`, `limits.memory: 4480Mi/8Gi` — todo muy por debajo de los topes. +- `kubectl describe limitrange -n ecommerce` → rangos por-contenedor + (16Mi–1Gi mem, 10m–2 cpu) compatibles con los `resources` definidos en + `medusa.yaml`. +- Sin eventos de `exceeded quota` en el namespace. +- **Descartada:** no había presión de cuota ni un contenedor rechazado por + LimitRange. + +### 5. DNS + +- `kubectl exec` en el pod viejo (sano) → `getent hosts postgres-svc` + resolvía a `10.42.0.98` correctamente. +- CoreDNS (`kube-system`) → `Running`, sano. +- Repetido el mismo chequeo **dentro del propio init container atascado** + (`wait-for-postgres` del pod nuevo) → misma resolución correcta, y + `/etc/resolv.conf` con `nameserver 10.43.0.10` normal. +- **Descartada:** la resolución DNS funcionaba de forma idéntica en el pod + sano y en el pod atascado. + +### 6. MTU/PMTUD cross-node (blackhole conocido, ver `docs/known-issues.md`) + +- Existe un blackhole de red confirmado (186/186 fallos reproducidos) cuando + un pod que necesita hablar con `commerce-postgres-0` queda agendado en un + nodo distinto al de Postgres, mitigado hoy con `podAffinity` en + `medusa.yaml`. +- `kubectl get pods -o wide` mostró que **los tres pods relevantes + (`commerce-postgres-0`, el pod viejo y el pod nuevo) estaban en el mismo + nodo** (`k3d-lab-cluster-server-0`). +- **Descartada para este incidente puntual:** al no haber tráfico + cross-node involucrado, el blackhole de MTU no podía ser la causa. Sigue + siendo una condición latente real del cluster (ver sección de deuda + técnica). + +## Causa raíz real + +El init container `wait-for-postgres` ejecutaba: + +```sh +until pg_isready -d "$DATABASE_URL" -t 5; do ... +``` + +pasando la URI completa de `DATABASE_URL` a `pg_isready`. En algún momento +se agregó el query param `?sslmode=disable` al `DATABASE_URL` generado por +`create-commerce-secrets.sh` (repo `scripts`, branch +`fix/medusa-database-url-sslmode`, ya ejecutada contra el secret vivo del +cluster). Con ese query param, el parser de conninfo de `pg_isready` en la +imagen `postgres:17-alpine` no lograba interpretar la URI y devolvía: + +``` +postgres-svc:5432 - no attempt +``` + +`exit 3` — **`no attempt`** es un status propio de `pg_isready` que indica +que el cliente **ni siquiera intentó conectar** por un problema en los +parámetros de conexión (a diferencia de `no response`, que sí implica un +intento de conexión fallido). Esto se reprodujo de forma determinística +ejecutando el mismo comando **dentro del propio init container atascado** +(sin crear pods efímeros nuevos), y se aisló la causa probando la misma URI: + +- con `pg_isready -h postgres-svc -p 5432 -U medusa -d medusa` (sin URI) → + `accepting connections`, exit 0. +- con la URI completa pero sin el `?sslmode=disable` → también exitosa. +- con la URI completa incluyendo `?sslmode=disable` → reproducía el fallo. + +Como el `until` no tiene timeout ni backoff máximo, el init container +quedaba reintentando indefinidamente sin nunca fallar de forma visible +(de ahí que no apareciera como CrashLoopBackOff). + +## Fix aplicado + +`workloads/ecommerce/commerce/medusa.yaml`, init container +`wait-for-postgres`: + +```diff +- until pg_isready -d "$DATABASE_URL" -t 5; do ++ until pg_isready -h postgres-svc -p 5432 -U "$POSTGRES_USER" -d "$POSTGRES_DB" -t 5; do +``` + +`postgres-svc` y `5432` son fijos (el Service de Postgres); `POSTGRES_USER` +y `POSTGRES_DB` ya llegan al contenedor vía el mismo `envFrom: +secretRef: commerce-secrets`. El chequeo de disponibilidad deja de depender +por completo del formato de `DATABASE_URL`, incluyendo cualquier query +param futuro. + +Commit `e9c93bf` en `main` de `apps-registry`, pusheado y sincronizado por +Argo CD (`ecommerce-app`, revisión `e9c93bfb82ec09b8ea2cc3468040ef9d72416542`, +sync a las `2026-08-10T05:29:32Z`). El pod atascado +`medusa-deploy-9d8784d7b-g9jql` fue reemplazado automáticamente por +`medusa-deploy-56c7c878b6-xgnkb`, que completó `wait-for-postgres` en +segundos y quedó `1/1 Running` sin intervención manual adicional. + +## Lecciones aprendidas + +**Separar el chequeo de disponibilidad (`wait-for-postgres`) de las +migraciones (`migrations`) en init containers distintos fue lo correcto** y +ya estaba así desde que se introdujo este init container +(commit `f4ad140`). Eso permitió aislar el fallo al primer init container +sin ambigüedad — la falla nunca llegó a tocar `migrations`. La lección no es +cambiar esa separación, sino mantenerla: cualquier chequeo de +disponibilidad debe usar el mínimo de parámetros necesarios (host/puerto/ +usuario/db) en vez de reusar una URI de conexión pensada para la app, +que puede cambiar de formato por razones ajenas al chequeo. + +**No fue necesario crear pods de diagnóstico efímeros, y no crearlos evitó +ruido adicional en el cluster.** Toda la reproducción y aislamiento de la +causa raíz (DNS, `pg_isready` con distintos argumentos, lectura de +`/etc/resolv.conf`) se hizo con `kubectl exec` sobre pods que ya existían +(el pod viejo sano, el pod nuevo atascado, y `commerce-postgres-0`). Crear +pods de prueba repetidos consume cuota (`ecommerce-quota` está en `pods: +7/12`, con margen pero no ilimitado) y puede introducir su propio ruido de +scheduling/red — como muestra `docs/known-issues.md`, el diagnóstico previo +del blackhole de MTU sí necesitó pods de prueba dedicados (186 intentos) +porque el fenómeno dependía del nodo de scheduling, algo que no se puede +observar desde un pod ya corriendo. La regla práctica: usar pods +existentes cuando el fallo es reproducible ahí, y reservar pods efímeros +para cuando la variable a probar (ej. nodo, imagen, versión) no se puede +cambiar en un pod ya desplegado — midiendo el impacto (cuota, ruido) de +cada corrida. + +## Deuda técnica pendiente (no resuelta por este incidente) + +El blackhole de red cross-node por MTU/PMTUD documentado en +`docs/known-issues.md` sigue sin arreglo de fondo (ajustar MTU a `1400` en +la red Docker/flannel del cluster k3d). El workaround actual es el +`podAffinity` en `medusa.yaml` que fija `medusa-deploy` al mismo nodo que +`commerce-postgres-0`. + +Existe una branch abierta, `fix/medusa-wait-for-postgres`, que: + +- Elimina ese `podAffinity`. +- Borra `docs/known-issues.md` por completo. +- **No incluye el fix de MTU real** (no toca configuración de flannel ni de + la red Docker del cluster). + +Mergear esa branch tal como está day-1 reintroduciría el blackhole cross-node +sin red de contención y borraría la única documentación del problema. No se +tocó como parte de este incidente. Queda pendiente decidir si se cierra sin +mergear, o si se retoma agregando primero el fix real de MTU antes de poder +quitar el `podAffinity` con seguridad.