Files
apps-registry/docs/playbooks/incidente-medusa-crashloop-2026-08.md
devops 381e6ab6ab 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.
2026-08-10 00:47:23 -05:00

9.9 KiB
Raw Permalink Blame History

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-securityPodSelector: <none> (aplica a todos los pods), Allowing ingress traffic: To Port: <any>, From: NamespaceSelector: <none> (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 ecommercepods: 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 (16Mi1Gi mem, 10m2 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:

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 3no 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:

-              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.