Files
apps-registry/workloads/docs-portal/docs/playbooks/migracion-platform-templates-2026-08.md
T
devopsandClaude Sonnet 5 837446ba16
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 19s
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Failing after 5m0s
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
2026-08-17 00:56:29 -05:00

5.1 KiB

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 (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 (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.