feat(docs-portal): pipeline GitOps y manifiestos K8s para el portal MkDocs
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.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
# 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-técnica-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.
|
||||
Reference in New Issue
Block a user