Agrega lo que faltaba para desplegar workloads/docs-portal/ (ya existente sin commitear): deployment/service/ingress + kustomization siguiendo el patrón de workloads/nginx, Applications de workload y gobernanza separadas, y el workflow de Gitea Actions (build+push a Gitea Registry, bump de versión en el manifiesto) siguiendo el mismo patrón que build.yaml/build-medusa.yaml.
7.3 KiB
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:
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 claveDATABASE_URLexiste, longitud correcta (~126 caracteres).pg_isreadyejecutado dentro del propio podcommerce-postgres-0con eseDATABASE_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
medusahaciapostgres-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 a10.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 deexceeded quota.- Descartada: no había presión de cuota.
5. DNS
getent hosts postgres-svcresolvía a10.42.0.98correctamente, 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 widemostró 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 abajo.
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). 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
!!! 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:
- 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.