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