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.
213 lines
9.9 KiB
Markdown
213 lines
9.9 KiB
Markdown
# 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.
|