docs(docs-portal): documentar platform-templates y la migracion real
Nueva seccion 'Templates' con la pagina principal de platform-templates (diagrama Mermaid del flujo codigo->produccion, los 3 arquetipos, el hallazgo real de Gitea sobre uses: cross-repo). Nuevo playbook de la migracion de docs-portal/frontend/medusa, con los 3 bugs reales encontrados en produccion (runAsNonRoot sin imagen no-root, capabilities rompiendo el chown de nginx, runAsNonRoot sin runAsUser numerico) y los 2 gotchas de Kustomize (rename via patch, anular un campo heredado). Nota: el directorio se llama docs/plantillas/ y no docs/templates/ -- MkDocs excluye silenciosamente cualquier carpeta llamada 'templates' del build (confirmado en vivo con mkdocs build --strict, sin ningun error/warning que lo explique, solo un INFO 'excluded from built site'). Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_011FqYP3Wf1W63Qmh7qgXAAT
This commit is contained in:
1 parent
8c79a045fd
commit
837446ba16
3 files changed
+189
No files matched your search
@@ -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<br/>(una app nueva)"] --> B["Elegir arquetipo:<br/>Next.js / API Node / sitio estático"]
|
||||||
|
B --> C["Copiar Dockerfile de<br/>platform-templates/dockerfiles/"]
|
||||||
|
C --> D["Agregar .gitea/workflows/build.yaml<br/>que llama al reusable workflow del arquetipo"]
|
||||||
|
D --> E["Copiar k8s-manifests/overlays-ejemplo/<br/>y ajustar nombre/imagen/puerto"]
|
||||||
|
E --> F["Generar par de llaves Cosign<br/>(cosign.pub commiteado, privada como secret)"]
|
||||||
|
F --> G["git push"]
|
||||||
|
G --> H["Pipeline: Gitleaks → build →<br/>Trivy → Semgrep → Syft → Cosign"]
|
||||||
|
H -->|"CRITICAL o firma inválida"| I["❌ Pipeline se detiene<br/>(nada llega al registry)"]
|
||||||
|
H -->|"todo pasa"| J["Imagen firmada<br/>sube al registry"]
|
||||||
|
J --> K["Pipeline actualiza el<br/>manifiesto GitOps automáticamente"]
|
||||||
|
K --> L["Argo CD detecta el cambio<br/>y sincroniza"]
|
||||||
|
L --> M["✅ App corriendo,<br/>con probes + resource limits +<br/>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.
|
||||||
@@ -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
|
||||||
|
<imagen> 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.
|
||||||
@@ -78,6 +78,8 @@ markdown_extensions:
|
|||||||
|
|
||||||
nav:
|
nav:
|
||||||
- Inicio: index.md
|
- Inicio: index.md
|
||||||
|
- Templates:
|
||||||
|
- platform-templates: plantillas/platform-templates.md
|
||||||
- Arquitectura:
|
- Arquitectura:
|
||||||
- Visión general: arquitectura/vision-general.md
|
- Visión general: arquitectura/vision-general.md
|
||||||
- Red y exposición: arquitectura/red-y-exposicion.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
|
- 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
|
- 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
|
- 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:
|
- Aprendizajes:
|
||||||
- Notas sueltas: aprendizajes/notas-sueltas.md
|
- Notas sueltas: aprendizajes/notas-sueltas.md
|
||||||
- Guía del estudiante:
|
- Guía del estudiante:
|
||||||
|
|||||||
Reference in new issue
Block a user