# 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: ` (aplica a todos los pods), `Allowing ingress traffic: To Port: , From: NamespaceSelector: ` (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.