Files
devops 22534d65f3 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:10:36 -05:00

176 lines
7.3 KiB
Markdown

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