# 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: ```text 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-security` → `PodSelector: `, 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](#deuda-técnica-pendiente) abajo. ## 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`). 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: ```text postgres-svc:5432 - no attempt ``` !!! danger "`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). 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`: ```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 `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.