Files
apps-registry/workloads/docs-portal/docs/playbooks/incidente-crashloop-medusa.md
T
devops 376eb6d46a fix(docs-portal): corregir anchor roto en playbook de crashloop de Medusa
mkdocs build --strict (run 95, job 107) falló: el link a '#deuda-técnica-pendiente'
no coincide con el anchor real que genera mkdocs, porque el slugify por
defecto de Python-Markdown normaliza tildes (NFKD + strip de combining marks)
antes de generar el id del heading. El heading '## Deuda técnica pendiente'
genera 'deuda-tecnica-pendiente' (sin tilde), no 'deuda-técnica-pendiente'.
2026-08-13 22:09:56 -05:00

7.3 KiB

Incidente: medusa-deploy atascado sin levantar (2026-08)

!!! info "Origen" Migrado desde apps-registry/docs/playbooks/incidente-medusa-crashloop-2026-08.md. Contenido técnico preservado sin cambios de fondo — solo formato adaptado a MkDocs Material.

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 siguió sirviendo tráfico sin interrupción (maxUnavailable: 0) — 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.

!!! tip "Lección" Un pod en Init:X/Y con 0 restarts y un CrashLoopBackOff requieren líneas de investigación distintas — y el primero no dispara las alertas típicas de CrashLoop, lo que probablemente explica por qué pasó desapercibido tanto tiempo. Verificar siempre el estado real con kubectl get pods antes de asumir el tipo de falla que describe quien reporta el incidente.

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, longitud correcta (~126 caracteres).
  • pg_isready ejecutado dentro del propio pod commerce-postgres-0 con ese DATABASE_URL: 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>, ingress allow-all, egress sin restricciones.
  • Descartada: no podía estar bloqueando la conexión de medusa hacia postgres-svc.

3. Salud de Postgres

  • 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.
  • Descartada: Postgres estaba sano.

4. ResourceQuota / LimitRange del namespace

  • pods: 7/12, requests.cpu: 625m/3, requests.memory: 1504Mi/4Gi — todo muy por debajo de los topes. Sin eventos de exceeded quota.
  • Descartada: no había presión de cuota.

5. DNS

  • getent hosts postgres-svc resolvía a 10.42.0.98 correctamente, tanto en el pod sano como dentro del propio init container atascado. CoreDNS sano.
  • Descartada: la resolución DNS funcionaba de forma idéntica en ambos pods.

6. MTU/PMTUD cross-node (blackhole conocido)

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 con podAffinity en medusa.yaml.

  • kubectl get pods -o wide mostró que los tres pods relevantes estaban en el mismo nodo (k3d-lab-cluster-server-0).
  • Descartada para este incidente puntual (sin tráfico cross-node involucrado) — pero sigue siendo una condición latente real del cluster. Ver nota de deuda técnica abajo.

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

!!! danger "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).

Aislamiento reproducido dentro del propio init container atascado:

  • pg_isready -h postgres-svc -p 5432 -U medusa -d medusa (sin URI) → accepting connections, exit 0.
  • URI completa sin ?sslmode=disable → también exitosa.
  • URI completa con ?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 envFrom: secretRef: commerce-secrets. El chequeo de disponibilidad deja de depender por completo del formato de DATABASE_URL.

Commit e9c93bf en main de apps-registry, sincronizado por Argo CD (ecommerce-app) a las 2026-08-10T05:29:32Z. El pod atascado fue reemplazado automáticamente por uno sano, sin intervención manual adicional.

Lecciones aprendidas

Separar el chequeo de disponibilidad de las migraciones en init containers distintos fue lo correcto — permitió aislar el fallo al primer init container sin ambigüedad. La lección no es cambiar esa separación, sino mantenerla: cualquier chequeo de disponibilidad debe usar el mínimo de parámetros necesarios en vez de reusar una URI de conexión pensada para la app.

No fue necesario crear pods de diagnóstico efímeros. Toda la reproducción se hizo con kubectl exec sobre pods que ya existían. Crear pods de prueba repetidos consume cuota y puede introducir su propio ruido de scheduling/red — la regla práctica es usar pods existentes cuando el fallo es reproducible ahí, y reservar pods efímeros para cuando la variable a probar (nodo, imagen, versión) no se puede cambiar en un pod ya desplegado.

Deuda técnica pendiente

El blackhole de red cross-node por MTU/PMTUD 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 y borra docs/known-issues.md sin incluir el fix de MTU real. Mergearla tal como está reintroduciría el blackhole cross-node sin red de contención. Queda pendiente decidir si se cierra sin mergear, o si se retoma agregando primero el fix real de MTU.