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.
9.9 KiB
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 claveDATABASE_URLexiste.- Se verificó longitud del valor (
~126caracteres, no vacío ni truncado) sin volcar el contenido en texto plano. - Se probó el
DATABASE_URLreal contra Postgres conpg_isreadyejecutado dentro del propio podcommerce-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
medusahaciapostgres-svc.
3. Salud de Postgres
kubectl exec -n ecommerce commerce-postgres-0 -- psql -U medusa -c "SELECT count(*) FROM pg_stat_activity;"→8conexiones activas, respuesta inmediata.kubectl get endpoints -n ecommerce postgres-svc→ apunta correctamente a10.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 losresourcesdefinidos enmedusa.yaml.- Sin eventos de
exceeded quotaen el namespace. - Descartada: no había presión de cuota ni un contenedor rechazado por LimitRange.
5. DNS
kubectl execen el pod viejo (sano) →getent hosts postgres-svcresolvía a10.42.0.98correctamente.- CoreDNS (
kube-system) →Running, sano. - Repetido el mismo chequeo dentro del propio init container atascado
(
wait-for-postgresdel pod nuevo) → misma resolución correcta, y/etc/resolv.confconnameserver 10.43.0.10normal. - 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-0queda agendado en un nodo distinto al de Postgres, mitigado hoy conpodAffinityenmedusa.yaml. kubectl get pods -o widemostró 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:
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:
- 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.mdpor 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.