docs(devsecops): documentar 4+1 hallazgos de infraestructura y TODOs de Cosign
5 playbooks nuevos en docs/playbooks/ del portal: - SQLite en modo DELETE causando SQLITE_BUSY, por que WAL lo resuelve. - Anchors YAML no soportados por el parser de Gitea Actions, incluido el diagnostico equivocado inicial (indentacion) que se corrigio despues -- documentado con honestidad como leccion de proceso. - Docker-in-Docker vs sibling containers, y por que Semgrep corre nativo en un venv en vez de como container aparte. - El SNI/realm HTTP del tunel rompiendo especificamente a Cosign (docker push funcionaba, cosign sign no), conectado con el incidente ya documentado del 504 -- misma capa de ingress, dos fallas distintas. - Contencion de recursos entre Gitea y su runner: causa raiz real confirmada en vivo durante esta misma sesion (ambos contenedores reiniciados a mitad de un build, sin limites de CPU/memoria reales), con fix propuesto (ver rama fix/gitea-runner-resource-limits en scripts/) pendiente de aplicacion manual. Se actualiza el playbook del 504 para linkear hacia el nuevo hallazgo de contencion, cerrando el loop que habia quedado abierto ahi. cosign.md: nueva seccion "Limitaciones conocidas y mejoras futuras" con los dos TODO reales -- firma por tag en vez de digest (con el warning real de Cosign visto en los logs) y transparency log de Rekor deshabilitado, explicado en simple. index.md: tabla resumen de los 3 pipelines (imagen + adaptacion de Semgrep por stack), diagrama generalizado para reflejar que es el mismo patron en los 3 workflows, y "que falta" actualizado. Validado con mkdocs build --strict (0 errores, 0 warnings) antes de commitear.
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
# Incidente: contención de recursos entre Gitea y su runner (2026-08)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Ventana del incidente** | 2026-08-15, durante el primer intento real de correr el pipeline de seguridad completo en `commerce-backend` |
|
||||
| **Servicio afectado** | `gitea` y `gitea-runner` (mismo host ZimaOS) |
|
||||
| **Impacto** | Un job de CI en curso (build pesado de Medusa + Trivy) fue cancelado a mitad de camino porque ambos contenedores se reiniciaron solos |
|
||||
|
||||
## Contexto
|
||||
|
||||
Este hallazgo estaba **anotado como pendiente** en el playbook del
|
||||
[504 del túnel](incidente-504-gitea-tunnel.md): una ventana de latencia
|
||||
alta y algunos 502/530 habían coincidido, en su momento, con una
|
||||
ejecución pesada de Gitea Actions — anotado como "contención de
|
||||
recursos local, no relacionado al fix del túnel, vigilar si es
|
||||
recurrente". Al triplicar la carga agregando el mismo pipeline de
|
||||
seguridad a `commerce-backend` y `docs-portal`, se volvió a ver en
|
||||
vivo, esta vez con evidencia suficiente para confirmar la causa.
|
||||
|
||||
## Síntoma
|
||||
|
||||
Un run de `build-medusa.yaml` (con Gitleaks ya en verde, build de
|
||||
Docker en curso) quedó cancelado a mitad de camino. El log del runner
|
||||
mostró, en la misma ventana de un par de minutos:
|
||||
|
||||
```text
|
||||
level=error msg="failed to fetch task" error="unavailable: 502 Bad Gateway"
|
||||
level=info msg="runner: ... shutdown initiated, waiting 0s for running jobs to complete before shutting down"
|
||||
level=warning msg="runner: ... cancelled in progress jobs during shutdown"
|
||||
level=info msg="Starting runner daemon"
|
||||
```
|
||||
|
||||
El mismo patrón se repitió una segunda vez pocos minutos después. En
|
||||
paralelo, `docker ps` mostró que el contenedor `gitea` también se
|
||||
había reiniciado casi al mismo tiempo.
|
||||
|
||||
## Diagnóstico
|
||||
|
||||
`docker inspect` sobre ambos contenedores en el momento del incidente
|
||||
mostró:
|
||||
|
||||
- `ExitCode=0` y `RestartCount=0` en los dos — **no fue un crash ni un
|
||||
OOM-kill** (eso dejaría un `ExitCode` distinto de 0 o
|
||||
`OOMKilled=true`, y el conteo de reinicios de la política de Docker
|
||||
subiría). El `StartedAt` cambió igual, lo que indica un reinicio
|
||||
limpio disparado por algo externo al propio proceso — muy
|
||||
probablemente el supervisor de apps de CasaOS (`zimaos-app-management`),
|
||||
aunque no se pudo confirmar por logs (`/var/log/casaos/log.log` es
|
||||
de acceso root-only, sin sudo sin password disponible en esta
|
||||
sesión).
|
||||
- Memoria del host en el momento del incidente: **~1.3 GB libres de
|
||||
15 GB totales**, con ~2 GB de swap en uso — el host estaba bajo
|
||||
presión real de memoria, no solo de CPU.
|
||||
- Ningún límite de recursos real en ninguno de los dos contenedores
|
||||
(ver detalle abajo).
|
||||
|
||||
## Causa raíz
|
||||
|
||||
Ni `gitea` ni `gitea-runner` tienen aislamiento de recursos real
|
||||
frente al resto del host:
|
||||
|
||||
| | `gitea` | `gitea-runner` |
|
||||
|---|---|---|
|
||||
| Límite de memoria | ~15.5 GB (≈ toda la RAM del host, no es un límite real) | **ninguno** |
|
||||
| Límite de CPU (cuota dura) | ninguno | ninguno |
|
||||
| Peso relativo de CPU (`cpu-shares`) | **90** (muy por debajo del default de Docker, 1024) | 0 → default Docker (**1024**) |
|
||||
|
||||
Con `gitea` en 90 y `gitea-runner` en el default de 1024, bajo
|
||||
contención de CPU **el runner tiene ~11x más prioridad que el propio
|
||||
servidor Gitea** — lo opuesto a lo deseable, ya que Gitea es el
|
||||
servicio que necesita seguir respondiendo peticiones HTTP (incluidas
|
||||
las del propio runner haciendo polling) mientras un build pesado corre.
|
||||
|
||||
Sumado a que `gitea-runner` no tiene techo de memoria: un job pesado
|
||||
(build de Docker de una imagen con `node_modules` grande + escaneo de
|
||||
Trivy con vuln + secret scanning) puede consumir memoria sin límite,
|
||||
empujando al host entero a swap y dejando lento/no-responsivo a todo
|
||||
lo demás — incluida la propia base de datos SQLite de Gitea (ver
|
||||
[incidente de SQLite en modo DELETE](incidente-sqlite-modo-delete-2026-08.md),
|
||||
que probablemente reaparezca con más frecuencia bajo este mismo tipo
|
||||
de presión, aunque WAL reduce el impacto).
|
||||
|
||||
`gitea-runner` tampoco corre gestionado por el mismo `docker-compose.yml`
|
||||
de Gitea — se crea con un `docker run` suelto desde
|
||||
`scripts/deploy-lab.sh` (`create_gitea_runner_container()`), montando
|
||||
`/var/run/docker.sock` directo (sibling containers, ver
|
||||
[incidente de Docker-in-Docker](incidente-docker-in-docker-semgrep-2026-08.md)).
|
||||
Esto no es la causa del incidente de recursos, pero significa que
|
||||
cualquier ajuste de límites tiene que aplicarse en dos lugares
|
||||
distintos: el `docker-compose.yml` de `gitea` y el script que crea
|
||||
`gitea-runner`.
|
||||
|
||||
!!! info "La concurrencia del runner ya estaba bien"
|
||||
`capacity: 1` en el `config.yaml` del runner ya limita la
|
||||
ejecución a **un solo job a la vez** — no es un problema de
|
||||
"demasiados jobs en paralelo". El problema es que incluso un solo
|
||||
job pesado, sin ningún límite de recursos, puede acaparar
|
||||
suficiente CPU/memoria como para dejar sin aire al resto del host.
|
||||
|
||||
## Fix propuesto (documentado, aplicación manual pendiente)
|
||||
|
||||
1. **`scripts/deploy-lab.sh`** (rama `fix/gitea-runner-resource-limits`,
|
||||
pendiente de mergear): agrega `--cpu-shares 512 --memory 8g
|
||||
--memory-swap 10g` a la creación de `gitea-runner`. Solo afecta a la
|
||||
próxima vez que se cree el contenedor (la función es idempotente),
|
||||
no al que ya está corriendo.
|
||||
2. **Fix inmediato en caliente** (sin recrear contenedores), a aplicar
|
||||
manualmente por fuera de este repo:
|
||||
|
||||
```bash
|
||||
docker update --cpu-shares 1024 gitea
|
||||
docker update --cpu-shares 512 --memory 8g --memory-swap 10g gitea-runner
|
||||
```
|
||||
|
||||
3. **`docker-compose.yml` de Gitea** (fuera de este repo, acceso
|
||||
root-only): subir `cpu_shares` de `gitea` de 90 a 1024 — pendiente
|
||||
de aplicar manualmente, no se implementó todavía.
|
||||
4. **Mover `gitea-runner` a k3d** como job/pod separado: evaluado como
|
||||
posible mejora de fondo (aislamiento real vía Kubernetes en vez de
|
||||
límites de Docker sueltos), pero queda **solo como recomendación
|
||||
documentada** — no implementado en esta vuelta.
|
||||
|
||||
## Validación
|
||||
|
||||
Con `gitea` y `gitea-runner` estables (sin reinicios) después del
|
||||
incidente, se reintentó el mismo pipeline y corrió de punta a punta
|
||||
sin interrupciones — pero eso confirma que el host se estabilizó
|
||||
después del pico de carga, **no** que el fix de límites de recursos ya
|
||||
esté aplicado (sigue pendiente, ver arriba). La correlación completa
|
||||
carga-pesada → reinicio ya se había visto antes, en dos fechas
|
||||
distintas (2026-07-30 y 2026-08-11) además de esta — no es un evento
|
||||
aislado, es un patrón recurrente que ahora tiene una causa raíz
|
||||
concreta y una propuesta de fix.
|
||||
Reference in New Issue
Block a user