# 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: <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](#deuda-tecnica-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.
