diff --git a/workloads/docs-portal/docs/plantillas/platform-templates.md b/workloads/docs-portal/docs/plantillas/platform-templates.md new file mode 100644 index 0000000..f42e969 --- /dev/null +++ b/workloads/docs-portal/docs/plantillas/platform-templates.md @@ -0,0 +1,74 @@ +# platform-templates: dar de alta una app nueva + +!!! info "Qué problema resuelve" + Antes de este repo, cada app nueva significaba escribir un + Dockerfile, un pipeline con los 5 pasos de DevSecOps (Gitleaks, + Trivy, Semgrep, Syft, Cosign), y los manifiestos de Kubernetes — + todo desde cero, copiando y adaptando a mano lo que ya existía en + otra app. `platform-templates` es el repo donde ese trabajo repetido + quedó hecho una sola vez: copiás una plantilla, completás unos + pocos valores (nombre, imagen, puerto), y tenés el mismo nivel de + seguridad y buenas prácticas que ya tienen `ecommerce-frontend`, + `commerce-backend` y `docs-portal` — sin tener que redescubrirlas. + +Repo: [`devops/platform-templates`](https://gitea.cruzcloud.net/devops/platform-templates) + +## De "tengo código" a "está corriendo en producción" + +```mermaid +flowchart TD + A["Tengo código
(una app nueva)"] --> B["Elegir arquetipo:
Next.js / API Node / sitio estático"] + B --> C["Copiar Dockerfile de
platform-templates/dockerfiles/"] + C --> D["Agregar .gitea/workflows/build.yaml
que llama al reusable workflow del arquetipo"] + D --> E["Copiar k8s-manifests/overlays-ejemplo/
y ajustar nombre/imagen/puerto"] + E --> F["Generar par de llaves Cosign
(cosign.pub commiteado, privada como secret)"] + F --> G["git push"] + G --> H["Pipeline: Gitleaks → build →
Trivy → Semgrep → Syft → Cosign"] + H -->|"CRITICAL o firma inválida"| I["❌ Pipeline se detiene
(nada llega al registry)"] + H -->|"todo pasa"| J["Imagen firmada
sube al registry"] + J --> K["Pipeline actualiza el
manifiesto GitOps automáticamente"] + K --> L["Argo CD detecta el cambio
y sincroniza"] + L --> M["✅ App corriendo,
con probes + resource limits +
securityContext ya puestos"] + + style A fill:#e8f4fd,stroke:#4a90d9 + style M fill:#e6f7e6,stroke:#4ba84b + style I fill:#fde8e8,stroke:#d94a4a +``` + +Los pasos A→F son manuales (una vez, al dar de alta la app). De G en +adelante es automático — cada `git push` a `main` repite H→M solo. + +## Los 3 arquetipos + +| Arquetipo | Para qué | Basado en | +|---|---|---| +| `archetype-nextjs-frontend` | Frontend Next.js con SSR (`output: standalone`) | `ecommerce-frontend` | +| `archetype-node-api` | API backend Node/Express, sin UI | `commerce-backend` (Medusa) | +| `archetype-static-site` | Sitio estático servido por nginx (MkDocs, Hugo, Astro) | `docs-portal` | + +Cada uno ya trae integrado el pipeline completo de DevSecOps — no hay +que agregar los 5 pasos por separado, vienen adentro del reusable +workflow. + +## Un detalle real de Gitea que no es como GitHub + +Armar este repo requirió confirmar en vivo (no asumir) cómo Gitea +Actions maneja las referencias cross-repo, porque **no es 1:1 igual a +GitHub Actions** en dos puntos concretos: + +1. Los reusable workflows (`workflow_call`) tienen que vivir en + `.gitea/workflows/` — no en cualquier carpeta. +2. Cualquier `uses:` que apunte a otro repo de esta misma instancia + necesita la URL completa (`https://gitea.cruzcloud.net/owner/repo/...`) + — la forma corta de GitHub (`owner/repo/...`) se resuelve contra + **github.com** acá, no contra Gitea local. + +El detalle completo, con el error real del servidor que confirmó cada +punto, está en el README del repo +([`pipelines/archetype-nextjs-frontend/README.md`](https://gitea.cruzcloud.net/devops/platform-templates/src/branch/main/pipelines/archetype-nextjs-frontend/README.md)). + +## Ver también + +- [Migración de docs-portal/frontend/medusa a platform-templates](../playbooks/migracion-platform-templates-2026-08.md) + — qué cambió en las 3 apps que ya existían, y los bugs reales que + aparecieron aplicándolo en producción. diff --git a/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md b/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md new file mode 100644 index 0000000..cba5acb --- /dev/null +++ b/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md @@ -0,0 +1,112 @@ +# Migración a platform-templates: docs-portal, frontend, medusa (2026-08) + +| | | +|---|---| +| **Ventana** | 2026-08-17 | +| **Servicios migrados** | `docs-portal`, `ecommerce-frontend` (`frontend-deploy`), `commerce-backend` (`medusa-deploy`) | +| **Impacto en producción** | Cero downtime en las 3 — cada rollout mantuvo el pod viejo sirviendo tráfico hasta que el nuevo estuvo sano | + +## Qué cambió + +Las 3 apps pasaron de manifiestos Kustomize sueltos y propios de cada +una a un overlay sobre el base común de +[platform-templates](../plantillas/platform-templates.md) +(`k8s-manifests/base/`, referenciado por URL de git remota, no una +copia local). El base agrega por default lo que ninguna de las 3 tenía +completo: `readinessProbe`/`livenessProbe` (docs-portal y frontend no +tenían ninguna) y `securityContext` a nivel pod y contenedor. + +Ningún nombre de recurso cambió (`docs-portal-deployment`, +`frontend-deploy`, `medusa-deploy`, sus Services, todo se mantuvo +idéntico) — importante para no romper el tracking de Argo CD ni la +resolución DNS interna de otros servicios que dependen de esos nombres +(ej. `MEDUSA_BACKEND_URL: http://medusa-svc:9000` en el frontend). + +## Por qué: 3 bugs reales encontrados aplicando esto en producción + +Los 3 se encontraron con el pod viejo todavía sirviendo tráfico +(`maxUnavailable: 0` en la estrategia de rollout, y Kubernetes excluye +automáticamente del Service cualquier pod que no pase su +`readinessProbe`) — cero impacto real en ningún momento, pero sí +bugs de verdad, no hipotéticos. + +### 1. `runAsNonRoot: true` contra una imagen que corre como root (docs-portal) + +`docs-portal` usa `nginx:1.27-alpine` sin ningún `USER` en su +Dockerfile — corre como root. Aplicar el default del base +(`runAsNonRoot: true`) rompió el pod al instante: +`CreateContainerConfigError`. Fix: override explícito +`runAsNonRoot: false` para este container puntual, documentado como +temporal hasta que `docs-portal` adopte +[`dockerfiles/static-nginx.Dockerfile`](https://gitea.cruzcloud.net/devops/platform-templates/src/branch/main/dockerfiles/static-nginx.Dockerfile) +(`nginxinc/nginx-unprivileged`, no-root de verdad) — cambio de imagen +aparte, no hecho todavía. + +### 2. `capabilities: drop: [ALL]` rompe el propio arranque de nginx (docs-portal) + +Con `runAsNonRoot: false` ya resuelto, el pod siguió fallando: + +``` +2026/08/17 05:30:33 [emerg] 1#1: chown("/var/cache/nginx/client_temp", 101) failed (1: Operation not permitted) +``` + +El entrypoint oficial de la imagen de nginx hace un `chown()` sobre su +propio directorio de cache en cada arranque — necesita `CAP_CHOWN`. +El base saca **todas** las capabilities por default; incluso corriendo +como root, sin `CAP_CHOWN` ese `chown()` falla igual. Fix: sacar la +restricción de capabilities entera para este container +(`capabilities: null` en el patch — es la única forma real de anular +un `drop: [ALL]` heredado, ver nota técnica más abajo). No es peor que +el comportamiento actual en producción, solo no está endurecido +todavía — se resuelve del todo con el mismo cambio de imagen del punto +1. + +### 3. `runAsNonRoot: true` sin `runAsUser` numérico (frontend) + +`ecommerce-frontend` sí corre no-root (`USER nextjs` en su Dockerfile) +— pero el pod nuevo falló igual: + +``` +Error: container has runAsNonRoot and image has non-numeric user (nextjs), +cannot verify user is non-root +``` + +El Dockerfile define el usuario por **nombre** (`useradd ... nextjs`), +no por UID numérico. Kubernetes no puede confirmar que ese nombre +resuelve a un UID distinto de 0 sin arrancar el contenedor, así que por +seguridad se niega a intentarlo. Fix: `runAsUser: 1001` explícito +(el UID real del `useradd --uid 1001` del Dockerfile). + +**Aplicado ya con este aprendizaje:** para `medusa-deploy`, se confirmó +el UID real del usuario `node` de la imagen con `docker run --rm + id node` (→ 1000) *antes* de escribir el overlay, en vez de +asumirlo — la migración de Medusa no tuvo ningún bug, arrancó sana en +el primer intento, `initContainers` (`wait-for-postgres`, `migrations`) +incluidos. + +## Nota técnica: 2 gotchas de Kustomize (no de Kubernetes) encontrados armando esto + +- **Renombrar un recurso vía patch de merge normal no funciona** — + aunque uses `target:` para seleccionar el recurso del base por su + nombre original, cambiar `metadata.name` dentro del contenido del + patch no lo renombra en el resultado final. Hace falta un patch + JSON6902 aparte (`op: replace, path: /metadata/name`). +- **Anular un campo heredado necesita `null` explícito** — para sacar + el `capabilities: {drop: [ALL]}` del base (bug #2 de arriba), ni + omitir el campo ni un objeto vacío `{}` alcanzan; solo + `capabilities: null` en el patch realmente lo borra. + +Ambos confirmados corriendo `kubectl kustomize` antes de aplicar nada +al cluster — no son suposiciones. + +## Validación + +Cada una de las 3 migraciones se confirmó con un `curl` real al dominio +público correspondiente después del rollout: + +- `docs.cruzcloud.net` → `200` +- `shop.cruzcloud.net` → `200` +- `commerce.cruzcloud.net/app` → `200` + +Y con el estado del pod nuevo (`kubectl get pods`, sin reinicios, +`1/1 Ready`) antes de dar cada migración por cerrada. diff --git a/workloads/docs-portal/mkdocs.yml b/workloads/docs-portal/mkdocs.yml index 1889766..bc9fcc6 100644 --- a/workloads/docs-portal/mkdocs.yml +++ b/workloads/docs-portal/mkdocs.yml @@ -78,6 +78,8 @@ markdown_extensions: nav: - Inicio: index.md + - Templates: + - platform-templates: plantillas/platform-templates.md - Arquitectura: - Visión general: arquitectura/vision-general.md - Red y exposición: arquitectura/red-y-exposicion.md @@ -99,6 +101,7 @@ nav: - Realm HTTP y Cosign: playbooks/incidente-cosign-realm-http-tunnel-2026-08.md - Contención de recursos Gitea/runner: playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md - Blackhole MTU cross-node: playbooks/incidente-mtu-blackhole-cross-node-2026-08.md + - "Migración a platform-templates": playbooks/migracion-platform-templates-2026-08.md - Aprendizajes: - Notas sueltas: aprendizajes/notas-sueltas.md - Guía del estudiante: