feat(docs-portal): pipeline GitOps y manifiestos K8s para el portal MkDocs #14
@@ -0,0 +1,125 @@
|
|||||||
|
name: Build and Push Docs Portal
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'workloads/docs-portal/docs/**'
|
||||||
|
- 'workloads/docs-portal/mkdocs.yml'
|
||||||
|
- 'workloads/docs-portal/requirements.txt'
|
||||||
|
- 'workloads/docs-portal/Dockerfile'
|
||||||
|
- '.gitea/workflows/deploy-docs.yaml'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Construir y publicar Docs Portal
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 20
|
||||||
|
|
||||||
|
env:
|
||||||
|
APP_DIR: workloads/docs-portal
|
||||||
|
MANIFEST_FILE: workloads/docs-portal/deployment.yaml
|
||||||
|
IMAGE_NAME: gitea.cruzcloud.net/devops/docs-portal
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout del código
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 1
|
||||||
|
persist-credentials: true
|
||||||
|
|
||||||
|
- name: Definir versión
|
||||||
|
id: vars
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "VERSION=v1.0.${{ github.run_number }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Validar secretos del Registry
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
test -n "${REGISTRY_USER}" || {
|
||||||
|
echo "ERROR: REGISTRY_USER no está configurado."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
test -n "${REGISTRY_PASSWORD}" || {
|
||||||
|
echo "ERROR: REGISTRY_PASSWORD no está configurado."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Login en Gitea Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: gitea.cruzcloud.net
|
||||||
|
username: ${{ secrets.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
logout: true
|
||||||
|
|
||||||
|
# mkdocs build --strict corre dentro del propio Dockerfile (stage de
|
||||||
|
# build), así que un nav/link roto rompe este paso antes de publicar.
|
||||||
|
- name: Construir y subir imagen
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: workloads/docs-portal/
|
||||||
|
file: workloads/docs-portal/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.VERSION }}
|
||||||
|
${{ env.IMAGE_NAME }}:latest
|
||||||
|
|
||||||
|
- name: Verificar promoción segura
|
||||||
|
id: promotion
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
|
||||||
|
CURRENT_SHA="${{ github.sha }}"
|
||||||
|
REMOTE_SHA="$(git rev-parse origin/main)"
|
||||||
|
|
||||||
|
if [ "${CURRENT_SHA}" = "${REMOTE_SHA}" ]; then
|
||||||
|
echo "promote=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "promote=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Hay un commit más reciente; no se actualizará el manifiesto."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Actualizar manifiesto GitOps
|
||||||
|
if: steps.promotion.outputs.promote == 'true'
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
VERSION="${{ steps.vars.outputs.VERSION }}"
|
||||||
|
|
||||||
|
git config user.name "gitea-actions"
|
||||||
|
git config user.email "[email protected]"
|
||||||
|
|
||||||
|
git fetch origin main
|
||||||
|
git checkout -B main origin/main
|
||||||
|
|
||||||
|
sed -i -E \
|
||||||
|
"s|(image: ${IMAGE_NAME}:).*|\1${VERSION}|g" \
|
||||||
|
"${MANIFEST_FILE}"
|
||||||
|
|
||||||
|
git add "${MANIFEST_FILE}"
|
||||||
|
|
||||||
|
if git diff --cached --quiet; then
|
||||||
|
echo "El manifiesto ya apunta a ${VERSION}."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git commit \
|
||||||
|
-m "chore(gitops): deploy Docs Portal ${VERSION} [skip ci]"
|
||||||
|
|
||||||
|
git push origin HEAD:main
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: docs-portal-app
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: https://gitea.cruzcloud.net/devops/apps-registry.git
|
||||||
|
path: workloads/docs-portal
|
||||||
|
targetRevision: main
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: docs-portal
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
apiVersion: argoproj.io/v1alpha1
|
||||||
|
kind: Application
|
||||||
|
metadata:
|
||||||
|
name: docs-portal-governance
|
||||||
|
namespace: argocd
|
||||||
|
spec:
|
||||||
|
project: default
|
||||||
|
source:
|
||||||
|
repoURL: https://gitea.cruzcloud.net/devops/platform-infra.git
|
||||||
|
path: docs-portal-governance
|
||||||
|
targetRevision: main
|
||||||
|
destination:
|
||||||
|
server: https://kubernetes.default.svc
|
||||||
|
namespace: docs-portal
|
||||||
|
syncPolicy:
|
||||||
|
automated:
|
||||||
|
prune: true
|
||||||
|
selfHeal: true
|
||||||
|
syncOptions:
|
||||||
|
- CreateNamespace=true
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
site/
|
||||||
|
.git/
|
||||||
|
**/__pycache__/
|
||||||
|
*.pyc
|
||||||
|
deployment.yaml
|
||||||
|
service.yaml
|
||||||
|
ingress.yaml
|
||||||
|
kustomization.yaml
|
||||||
|
README.md
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# --- Stage 1: build del sitio estático con MkDocs Material ---
|
||||||
|
FROM python:3.12-slim AS build
|
||||||
|
|
||||||
|
WORKDIR /site
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY mkdocs.yml .
|
||||||
|
COPY docs/ docs/
|
||||||
|
|
||||||
|
# --strict: cualquier link roto o warning de nav rompe el build,
|
||||||
|
# para no publicar nunca un sitio con referencias muertas.
|
||||||
|
RUN mkdocs build --strict
|
||||||
|
|
||||||
|
# --- Stage 2: sirve el sitio estático generado con nginx ---
|
||||||
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
|
COPY --from=build /site/site /usr/share/nginx/html
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: docs-portal-deployment
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: docs-portal
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: docs-portal
|
||||||
|
spec:
|
||||||
|
imagePullSecrets:
|
||||||
|
- name: gitea-registry-secret
|
||||||
|
containers:
|
||||||
|
- name: docs-portal
|
||||||
|
image: gitea.cruzcloud.net/devops/docs-portal:v1.0.0
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 64Mi
|
||||||
|
limits:
|
||||||
|
cpu: 200m
|
||||||
|
memory: 128Mi
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Notas sueltas / aprendizajes
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** esto es un scratchpad estructurado, no un
|
||||||
|
> playbook — va creciendo con notas cortas a medida que aparecen. Las
|
||||||
|
> entradas de abajo son semillas reales extraídas de incidentes ya
|
||||||
|
> documentados; expandir cada una con más contexto en fases siguientes.
|
||||||
|
|
||||||
|
## Qué haría distinto
|
||||||
|
|
||||||
|
- **Fijar `TUNNEL_TRANSPORT_PROTOCOL=http2` desde el día uno**, no
|
||||||
|
después de un incidente — QUIC/UDP sobre NAT doméstico fue un problema
|
||||||
|
predecible en retrospectiva. Ver [ADR 0003](../decisiones/0003-por-que-http2-sobre-quic-tunnel.md).
|
||||||
|
- **Separar siempre "chequeo de disponibilidad" de "migraciones"** en
|
||||||
|
init containers distintos, y que el chequeo use el mínimo de
|
||||||
|
parámetros posible (host/puerto/usuario/db) en vez de reusar una URI
|
||||||
|
de conexión pensada para la app. Confirmado como decisión correcta en
|
||||||
|
el [incidente de crashloop de Medusa](../playbooks/incidente-crashloop-medusa.md).
|
||||||
|
|
||||||
|
## Gotchas de Medusa v2
|
||||||
|
|
||||||
|
- El Store API (`/store/products`, validado por `StoreGetProductsParams`,
|
||||||
|
`.strict()`) **no acepta** `currency_code` ni `country_code` para
|
||||||
|
resolver precios calculados — solo `region_id` explícito. Ver
|
||||||
|
[playbook de precios](../playbooks/incidente-precios-medusa.md).
|
||||||
|
- El campo `thumbnail` de un producto **no se autocompleta** a partir
|
||||||
|
del array `images` — hay que setearlo explícito desde el Admin si el
|
||||||
|
frontend lo usa como imagen hero en listados.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Notas sobre MinIO / S3 provider.
|
||||||
|
- Notas sobre el subscriber `revalidate-storefront.ts` y sus límites.
|
||||||
|
- Qué automatizaría si volviera a montar este lab desde cero.
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Del commit al despliegue: flujo GitOps
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** narrar cada paso con ejemplos reales tomados de
|
||||||
|
> `.gitea/workflows/build-medusa.yaml` y `build.yaml`. El diagrama de
|
||||||
|
> secuencia de abajo refleja el pipeline real (build → push imagen →
|
||||||
|
> promoción segura → commit del tag → sync de Argo CD).
|
||||||
|
|
||||||
|
## Secuencia: un commit hasta quedar corriendo en el cluster
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor Dev as Devops
|
||||||
|
participant Gitea
|
||||||
|
participant CI as Gitea Actions
|
||||||
|
participant Reg as Gitea Registry
|
||||||
|
participant Argo as Argo CD
|
||||||
|
participant K8s as Cluster k3d
|
||||||
|
|
||||||
|
Dev->>Gitea: push a fix/<rama>
|
||||||
|
Dev->>Gitea: abre PR → main
|
||||||
|
Gitea->>Gitea: merge a main
|
||||||
|
Gitea->>CI: dispara workflow (paths: workloads/**)
|
||||||
|
CI->>CI: build de la imagen (Docker)
|
||||||
|
CI->>Reg: push imagen vX.Y.Z
|
||||||
|
CI->>CI: verifica promoción segura (HEAD == origin/main)
|
||||||
|
CI->>Gitea: commit "chore(gitops): release vX.Y.Z" actualizando el manifiesto
|
||||||
|
Argo->>Gitea: detecta el nuevo commit (poll/webhook)
|
||||||
|
Argo->>K8s: sync (prune:true, selfHeal:true)
|
||||||
|
K8s->>K8s: rollout del nuevo pod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Por qué esta separación (build vs. deploy)
|
||||||
|
|
||||||
|
- El **pipeline de CI** solo construye y publica la imagen — nunca
|
||||||
|
aplica nada directo al cluster.
|
||||||
|
- El **commit al manifiesto** (`sed` sobre el tag de imagen) es lo único
|
||||||
|
que representa "intención de desplegar" — y vive en el mismo repo Git
|
||||||
|
que Argo CD vigila.
|
||||||
|
- **Argo CD** es la única pieza con permisos de escritura sobre el
|
||||||
|
cluster real. Nada fuera de GitOps toca `kubectl apply` en producción.
|
||||||
|
- El paso de "promoción segura" (comparar `HEAD` contra `origin/main`)
|
||||||
|
evita que un pipeline viejo sobreescriba el tag con una versión
|
||||||
|
anterior si hubo un push más reciente mientras corría.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Ejemplo real con hashes de commit (tomar del historial de
|
||||||
|
`apps-registry`).
|
||||||
|
- Explicar `syncPolicy.automated.selfHeal` con un caso concreto (qué
|
||||||
|
pasa si alguien hace `kubectl edit` a mano).
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Glosario
|
||||||
|
|
||||||
|
> Pensado para alguien que ve estos términos por primera vez. Definiciones
|
||||||
|
> cortas primero, detalle técnico después. Se irá expandiendo en fases
|
||||||
|
> siguientes — por ahora cubre los términos que ya aparecen en el resto
|
||||||
|
> del sitio.
|
||||||
|
|
||||||
|
## GitOps
|
||||||
|
|
||||||
|
**En una frase:** el estado deseado del sistema vive en Git, y un
|
||||||
|
proceso automático (no una persona con `kubectl`) se encarga de que el
|
||||||
|
cluster real coincida con lo que dice Git.
|
||||||
|
|
||||||
|
**Detalle:** en vez de ejecutar comandos contra el cluster a mano, se
|
||||||
|
edita un archivo YAML, se hace commit, y una herramienta (aquí, Argo CD)
|
||||||
|
detecta el cambio y lo aplica. Si alguien cambia algo directo en el
|
||||||
|
cluster sin pasar por Git, GitOps lo revierte (`selfHeal`) — Git es la
|
||||||
|
única fuente de verdad.
|
||||||
|
|
||||||
|
## Tunnel (Cloudflare Tunnel)
|
||||||
|
|
||||||
|
**En una frase:** una forma de exponer un servicio interno a internet
|
||||||
|
sin abrir puertos en el router/firewall.
|
||||||
|
|
||||||
|
**Detalle:** un proceso (`cloudflared`) corre dentro de la red local y
|
||||||
|
abre una conexión saliente hacia Cloudflare. El tráfico público llega a
|
||||||
|
Cloudflare y viaja hacia adentro por esa misma conexión — no hace falta
|
||||||
|
port-forwarding ni IP pública propia.
|
||||||
|
|
||||||
|
## Argo CD
|
||||||
|
|
||||||
|
**En una frase:** el "robot" que sincroniza el cluster Kubernetes con
|
||||||
|
lo que está escrito en Git.
|
||||||
|
|
||||||
|
**Detalle:** vigila uno o más repos Git, compara el estado declarado
|
||||||
|
(manifiestos YAML/Kustomize) contra el estado real del cluster, y
|
||||||
|
aplica la diferencia. Ver [flujo GitOps](gitops-flujo.md).
|
||||||
|
|
||||||
|
## Manifest (manifiesto)
|
||||||
|
|
||||||
|
**En una frase:** un archivo YAML que describe qué quieres que exista
|
||||||
|
en Kubernetes (un Deployment, un Service, un Ingress...).
|
||||||
|
|
||||||
|
**Detalle:** Kubernetes no se controla escribiendo código imperativo
|
||||||
|
("crea un pod"), sino declarando el resultado deseado ("quiero 2
|
||||||
|
réplicas de esta imagen corriendo"). El manifiesto es ese documento.
|
||||||
|
|
||||||
|
## App of Apps
|
||||||
|
|
||||||
|
**En una frase:** un patrón donde una Application de Argo CD no
|
||||||
|
despliega una app directamente, sino que despliega *otras*
|
||||||
|
Applications.
|
||||||
|
|
||||||
|
**Detalle:** en este lab, `application.yaml` (raíz) vigila la carpeta
|
||||||
|
`apps/` de `apps-registry`; cada archivo ahí dentro es una Application
|
||||||
|
hija que a su vez apunta a un workload real. Permite agregar una app
|
||||||
|
nueva con un solo archivo YAML nuevo.
|
||||||
|
|
||||||
|
## Kustomize
|
||||||
|
|
||||||
|
**En una frase:** una forma de componer manifiestos de Kubernetes sin
|
||||||
|
plantillas (a diferencia de Helm).
|
||||||
|
|
||||||
|
**Detalle:** un archivo `kustomization.yaml` lista qué recursos YAML
|
||||||
|
incluir y qué parches aplicarles, sin necesidad de un lenguaje de
|
||||||
|
templating separado.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Namespace, ResourceQuota, LimitRange, NetworkPolicy
|
||||||
|
- Ingress vs. Service vs. Deployment
|
||||||
|
- StatefulSet (por qué Postgres lo usa y el frontend no)
|
||||||
|
- Init container
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Red y exposición
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** explicar cada dominio, por qué está proxied vs
|
||||||
|
> DNS-only en Cloudflare, y el manejo de TLS extremo a extremo. Tabla
|
||||||
|
> de dominios abajo es real, pendiente de expandir con capturas/ejemplos.
|
||||||
|
|
||||||
|
## Dominios activos
|
||||||
|
|
||||||
|
| Dominio | Servicio | Namespace | TLS |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `shop.cruzcloud.net` | Frontend Next.js (storefront) | `ecommerce` | Cloudflare (edge) |
|
||||||
|
| `commerce.cruzcloud.net` | Medusa Store/Admin API | `ecommerce` | Cloudflare (edge) |
|
||||||
|
| `media.cruzcloud.net` | MinIO (imágenes de producto) | `ecommerce` | Cloudflare (edge) |
|
||||||
|
| `gitea.cruzcloud.net` | Gitea | — | Cloudflare (edge) |
|
||||||
|
| `docs.cruzcloud.net` | Este portal | `docs` | Cloudflare (edge) |
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
- Diagrama de TLS termination (dónde termina TLS realmente: en
|
||||||
|
Cloudflare, no en Traefik — el tráfico interno NPM→k3d es HTTP plano).
|
||||||
|
- Explicar por qué el transporte del túnel se fijó a HTTP/2 en vez de
|
||||||
|
QUIC (ver [ADR 0003](../decisiones/0003-por-que-http2-sobre-quic-tunnel.md)
|
||||||
|
y el [playbook del 504](../playbooks/incidente-504-gitea-tunnel.md)).
|
||||||
|
- Patrón de diagnóstico "interno sano, público roto" (ver
|
||||||
|
[playbook de imágenes NPM](../playbooks/incidente-imagenes-npm.md)).
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
# Visión general
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** narrativa completa explicando cada capa del
|
||||||
|
> diagrama, con foco en el "por qué" de cada frontera de confianza. El
|
||||||
|
> diagrama de abajo es real (validado contra `docs/known-issues.md` y
|
||||||
|
> los playbooks de incidentes de `apps-registry`), pero el texto que lo
|
||||||
|
> acompaña es solo el esqueleto.
|
||||||
|
|
||||||
|
## Flujo de una petición pública
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
User(["Usuario"]) -->|HTTPS| CF["Cloudflare<br/>(DNS proxied + túnel)"]
|
||||||
|
CF -->|HTTP/2, red host| CFD["cloudflared<br/>(app ZimaOS)"]
|
||||||
|
CFD --> NPM["Nginx Proxy Manager<br/>192.168.68.61:90"]
|
||||||
|
NPM --> LB["k3d-lab-cluster-serverlb<br/>(k3d-proxy, 90→80)"]
|
||||||
|
LB --> TR["Traefik<br/>(ingress del cluster)"]
|
||||||
|
TR -->|Host: shop.cruzcloud.net| FE["frontend-svc<br/>(Next.js)"]
|
||||||
|
TR -->|Host: commerce.cruzcloud.net| MED["medusa-svc<br/>(Medusa API)"]
|
||||||
|
TR -->|Host: media.cruzcloud.net| MIN["minio-svc<br/>(imágenes de producto)"]
|
||||||
|
TR -->|Host: gitea.cruzcloud.net| GIT["Gitea"]
|
||||||
|
TR -->|Host: docs.cruzcloud.net| DOCS["docs-portal-svc<br/>(este sitio)"]
|
||||||
|
|
||||||
|
subgraph GitOps["Control plane GitOps"]
|
||||||
|
ARGO["Argo CD"] -->|sync| FE
|
||||||
|
ARGO -->|sync| MED
|
||||||
|
ARGO -->|sync| DOCS
|
||||||
|
GIT -.->|watch repos| ARGO
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Componentes
|
||||||
|
|
||||||
|
| Capa | Qué es | Dónde vive |
|
||||||
|
|---|---|---|
|
||||||
|
| Cloudflare Tunnel | Expone `*.cruzcloud.net` a internet sin abrir puertos en el NAS | Fuera del cluster, app nativa ZimaOS (`cloudflared`) |
|
||||||
|
| Nginx Proxy Manager | Enruta por dominio hacia el cluster | Fuera del cluster, contenedor `nginxproxymanager` |
|
||||||
|
| k3d | Cluster Kubernetes local (multi-nodo, Docker) | Host ZimaOS |
|
||||||
|
| Traefik | Ingress controller del cluster | Dentro de k3d |
|
||||||
|
| Argo CD | Sincroniza el estado del cluster con Git (patrón App of Apps) | Namespace `argocd` |
|
||||||
|
| Gitea | Origen de verdad de los manifiestos y del código de las apps | Contenedor propio, expuesto vía túnel |
|
||||||
|
|
||||||
|
Ver también: [Red y exposición](red-y-exposicion.md) · [Flujo GitOps](gitops-flujo.md) · [Glosario](glosario.md)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ADR 0001 — Por qué k3d (y no k3s directo, minikube, o un cluster cloud)
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** completar contexto/opciones/consecuencias con
|
||||||
|
> criterio real (recursos del NAS ZimaOS, necesidad de multi-nodo para
|
||||||
|
> reproducir el blackhole de MTU documentado en `known-issues.md`,
|
||||||
|
> costo cero vs. cluster gestionado).
|
||||||
|
|
||||||
|
**Estado:** aceptada (placeholder — pendiente de redactar)
|
||||||
|
**Fecha:** TODO
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Opciones consideradas
|
||||||
|
|
||||||
|
| Opción | A favor | En contra |
|
||||||
|
|---|---|---|
|
||||||
|
| k3d | | |
|
||||||
|
| k3s directo (sin Docker) | | |
|
||||||
|
| minikube | | |
|
||||||
|
| Cluster gestionado (cloud) | | |
|
||||||
|
|
||||||
|
## Decisión
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
TODO — mencionar explícitamente el blackhole de red cross-node
|
||||||
|
(MTU/PMTUD) como consecuencia real y documentada de correr multi-nodo
|
||||||
|
sobre Docker-in-Docker sin ajuste de MTU (ver
|
||||||
|
[playbook del crashloop de Medusa](../playbooks/incidente-crashloop-medusa.md)).
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# ADR 0002 — Por qué Argo CD (y no `kubectl apply` manual o un script de CI)
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** completar con el criterio real detrás de elegir
|
||||||
|
> reconciliación continua (`selfHeal`) sobre despliegue imperativo desde
|
||||||
|
> CI.
|
||||||
|
|
||||||
|
**Estado:** aceptada (placeholder — pendiente de redactar)
|
||||||
|
**Fecha:** TODO
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Opciones consideradas
|
||||||
|
|
||||||
|
| Opción | A favor | En contra |
|
||||||
|
|---|---|---|
|
||||||
|
| Argo CD (GitOps, pull-based) | | |
|
||||||
|
| `kubectl apply` manual | | |
|
||||||
|
| `kubectl apply` desde el pipeline de CI (push-based) | | |
|
||||||
|
|
||||||
|
## Decisión
|
||||||
|
|
||||||
|
TODO
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
TODO — mencionar el patrón App of Apps (`application.yaml` +
|
||||||
|
`apps/*.yaml`) y cómo `selfHeal:true` cambia el modelo mental de
|
||||||
|
"quién puede tocar el cluster".
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# ADR 0003 — Por qué HTTP/2 sobre QUIC para el transporte del túnel de Cloudflare
|
||||||
|
|
||||||
|
**Estado:** aceptada
|
||||||
|
**Fecha:** 2026-08 (ventana del incidente documentado abajo)
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** expandir la narrativa; el contexto/decisión ya
|
||||||
|
> están fundamentados en un incidente real, solo falta pulir la
|
||||||
|
> redacción y agregar los datos de validación completos.
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
`cloudflared` (el proceso que abre el túnel hacia Cloudflare) usaba
|
||||||
|
QUIC (UDP) como protocolo de transporte por defecto para sus 4
|
||||||
|
conexiones hacia el edge de Cloudflare. Una de esas conexiones sufría
|
||||||
|
fallos recurrentes de "control stream" cada 2-6 minutos, causando
|
||||||
|
`504 Gateway Timeout` intermitentes en **todos** los subdominios detrás
|
||||||
|
del túnel (Gitea, Argo CD, la tienda, etc.), no solo uno.
|
||||||
|
|
||||||
|
Diagnóstico completo en el
|
||||||
|
[playbook del incidente](../playbooks/incidente-504-gitea-tunnel.md).
|
||||||
|
|
||||||
|
## Opciones consideradas
|
||||||
|
|
||||||
|
| Opción | A favor | En contra |
|
||||||
|
|---|---|---|
|
||||||
|
| QUIC (default) | Menor latencia teórica, multiplexing sin head-of-line blocking | Sensible a NAT/firewall doméstico; causaba el 504 intermitente real |
|
||||||
|
| HTTP/2 sobre TCP | Estable en redes domésticas con NAT agresivo | Ligeramente más latencia teórica que QUIC |
|
||||||
|
|
||||||
|
## Decisión
|
||||||
|
|
||||||
|
Forzar `TUNNEL_TRANSPORT_PROTOCOL=http2` en la configuración del
|
||||||
|
contenedor `cloudflared`, en vez de dejar el default (QUIC/UDP).
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
- Se eliminaron los `504` intermitentes — validado con curl en loop
|
||||||
|
contra `gitea.cruzcloud.net`: ~0.37–0.4s de latencia estable por
|
||||||
|
24+ minutos sin un solo error.
|
||||||
|
- Se renuncia a la ventaja teórica de latencia de QUIC, aceptable en un
|
||||||
|
lab doméstico donde la estabilidad importa más que microsegundos.
|
||||||
|
- Si en el futuro cambia el hardware de red (router, NAT) podría valer
|
||||||
|
la pena revisar si QUIC vuelve a ser viable — no hay una razón de
|
||||||
|
fondo para excluirlo para siempre, solo evidencia de que falló en
|
||||||
|
esta red concreta.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# ADR NNNN — Título corto de la decisión
|
||||||
|
|
||||||
|
> Copiar este archivo como `NNNN-titulo-corto.md` (siguiente número
|
||||||
|
> disponible) para cada decisión arquitectónica nueva.
|
||||||
|
|
||||||
|
**Estado:** propuesta | aceptada | reemplazada por ADR-XXXX
|
||||||
|
**Fecha:** AAAA-MM-DD
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
¿Qué problema forzó esta decisión? ¿Qué restricciones reales existían
|
||||||
|
(hardware del NAS, presupuesto cero, tiempo disponible, conocimiento
|
||||||
|
previo)? Sin justificar todavía la elección — solo la situación.
|
||||||
|
|
||||||
|
## Opciones consideradas
|
||||||
|
|
||||||
|
| Opción | A favor | En contra |
|
||||||
|
|---|---|---|
|
||||||
|
| Opción A | | |
|
||||||
|
| Opción B | | |
|
||||||
|
| Opción C | | |
|
||||||
|
|
||||||
|
## Decisión
|
||||||
|
|
||||||
|
Qué se eligió, en una o dos frases.
|
||||||
|
|
||||||
|
## Consecuencias
|
||||||
|
|
||||||
|
- Qué se gana
|
||||||
|
- Qué se sacrifica o queda como deuda técnica
|
||||||
|
- Qué tendría que pasar para revertir esta decisión
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Guía del estudiante — ruta de aprendizaje sugerida
|
||||||
|
|
||||||
|
> Escrita para alguien que **nunca ha visto GitOps ni Kubernetes**. Si
|
||||||
|
> ya conoces estos conceptos, probablemente quieras saltar directo a
|
||||||
|
> [Arquitectura](../arquitectura/vision-general.md) o
|
||||||
|
> [Decisiones](../decisiones/0001-por-que-k3d.md).
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** convertir esto en una ruta paso a paso completa,
|
||||||
|
> con ejercicios o preguntas de verificación al final de cada parada.
|
||||||
|
> Por ahora es el esqueleto de la ruta.
|
||||||
|
|
||||||
|
## Antes de empezar
|
||||||
|
|
||||||
|
No hace falta saber Kubernetes de antemano. Sí ayuda tener claro qué es
|
||||||
|
un contenedor Docker — si eso todavía no es familiar, empieza por ahí
|
||||||
|
antes de seguir.
|
||||||
|
|
||||||
|
## Ruta sugerida
|
||||||
|
|
||||||
|
1. **[Conceptos básicos](conceptos-basicos.md)** — qué es un contenedor,
|
||||||
|
qué es Kubernetes, qué problema resuelve.
|
||||||
|
2. **[Glosario](../arquitectura/glosario.md)** — términos clave
|
||||||
|
(GitOps, tunnel, Argo CD, manifest) explicados en una frase antes del
|
||||||
|
detalle técnico.
|
||||||
|
3. **[Visión general de arquitectura](../arquitectura/vision-general.md)**
|
||||||
|
— el diagrama completo: cómo llega una petición desde el navegador
|
||||||
|
hasta la aplicación real.
|
||||||
|
4. **[Flujo GitOps](../arquitectura/gitops-flujo.md)** — qué pasa
|
||||||
|
exactamente entre que alguien hace `git push` y el cambio queda
|
||||||
|
corriendo.
|
||||||
|
5. **Un playbook real** — empieza por
|
||||||
|
[el incidente del crashloop de Medusa](../playbooks/incidente-crashloop-medusa.md):
|
||||||
|
es un buen ejemplo de cómo se descarta una hipótesis con evidencia en
|
||||||
|
vez de adivinar.
|
||||||
|
6. **Una decisión de arquitectura (ADR)** — lee
|
||||||
|
[por qué k3d](../decisiones/0001-por-que-k3d.md) para ver cómo se
|
||||||
|
documenta un trade-off real, no solo la elección final.
|
||||||
|
|
||||||
|
## Si esto se convierte en proyecto de grado
|
||||||
|
|
||||||
|
Este lab es candidato razonable de base para un proyecto de grado
|
||||||
|
porque ya tiene: infraestructura real corriendo, incidentes reales
|
||||||
|
documentados con causa raíz, y decisiones arquitectónicas explícitas
|
||||||
|
en vez de implícitas. La sección de
|
||||||
|
[aprendizajes](../aprendizajes/notas-sueltas.md) es un buen punto de
|
||||||
|
partida para encontrar preguntas de investigación abiertas (deuda
|
||||||
|
técnica, mejoras pendientes) en vez de inventar un tema desde cero.
|
||||||
|
|
||||||
|
**TODO:** definir aquí, en una sesión futura, el alcance concreto de
|
||||||
|
ese posible proyecto de grado (¿extender el lab? ¿documentar y
|
||||||
|
analizar el patrón? ¿construir sobre él?).
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# Conceptos básicos
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** este es el archivo con más responsabilidad
|
||||||
|
> pedagógica del sitio — pensado para alguien en su primer contacto con
|
||||||
|
> estos temas. Requiere más tiempo de redacción y ejemplos visuales que
|
||||||
|
> el resto; se deja como esqueleto a propósito para trabajarlo con
|
||||||
|
> cuidado en una sesión dedicada.
|
||||||
|
|
||||||
|
## Lo que este capítulo va a cubrir
|
||||||
|
|
||||||
|
- ¿Qué es un contenedor? (analogía antes que definición técnica)
|
||||||
|
- ¿Qué problema resuelve Kubernetes que Docker solo no resuelve?
|
||||||
|
- ¿Qué es un "cluster" en la práctica?
|
||||||
|
- ¿Qué es un namespace, y por qué importa?
|
||||||
|
- Diagrama Mermaid: "una app en un solo contenedor" vs. "una app en
|
||||||
|
Kubernetes" — mismo resultado final, distinta forma de lograrlo.
|
||||||
|
|
||||||
|
## TODO
|
||||||
|
|
||||||
|
Contenido pendiente — ver [Glosario](../arquitectura/glosario.md)
|
||||||
|
mientras tanto para definiciones cortas de los términos que van a
|
||||||
|
aparecer aquí.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# CruzCloud Lab — Plataforma GitOps
|
||||||
|
|
||||||
|
> **TODO (fase 2+):** reescribir esta portada con la narrativa final. Por
|
||||||
|
> ahora describe el propósito y la audiencia para que la navegación y el
|
||||||
|
> `nav:` de `mkdocs.yml` tengan un punto de entrada real.
|
||||||
|
|
||||||
|
Este sitio documenta un laboratorio personal de **Platform Engineering**:
|
||||||
|
un cluster Kubernetes (k3d) corriendo sobre un NAS (ZimaOS), gobernado
|
||||||
|
100% por GitOps (Gitea + Argo CD), sirviendo una tienda de e-commerce real
|
||||||
|
(Medusa + Next.js) como carga de trabajo de referencia.
|
||||||
|
|
||||||
|
No es un tutorial genérico ni una demo de juguete: cada decisión, cada
|
||||||
|
incidente y cada diagrama de este sitio corresponde a un sistema que
|
||||||
|
corre de verdad, con los commits, logs y causas raíz reales que lo
|
||||||
|
probaron.
|
||||||
|
|
||||||
|
## Para quién es este sitio
|
||||||
|
|
||||||
|
Este portal sirve a tres audiencias distintas, y está organizado para
|
||||||
|
que cada una pueda entrar por su propia puerta:
|
||||||
|
|
||||||
|
- **Reclutadores / pares de Platform Engineering** — ver
|
||||||
|
[`decisiones/`](decisiones/0001-por-que-k3d.md) para el criterio
|
||||||
|
arquitectónico detrás del stack, y [`playbooks/`](playbooks/incidente-crashloop-medusa.md)
|
||||||
|
para diagnóstico real de incidentes (no solo "lo arreglé").
|
||||||
|
- **Alguien evaluando este patrón para un caso de negocio real**
|
||||||
|
(tienda desplegable desde ZimaOS) — ver
|
||||||
|
[`arquitectura/`](arquitectura/vision-general.md) para el flujo
|
||||||
|
completo y su costo/complejidad real.
|
||||||
|
- **Estudiantes empezando con GitOps/Kubernetes** (incluye a mi hijo,
|
||||||
|
7mo semestre de Ingeniería de Sistemas) — empezar por
|
||||||
|
[`guia-estudiante/`](guia-estudiante/README.md), pensada para alguien
|
||||||
|
que nunca ha visto estos conceptos.
|
||||||
|
|
||||||
|
## Mapa del sitio
|
||||||
|
|
||||||
|
| Sección | Qué encontrarás |
|
||||||
|
|---|---|
|
||||||
|
| [Arquitectura](arquitectura/vision-general.md) | Diagramas Mermaid del flujo completo, red/exposición, flujo GitOps, glosario |
|
||||||
|
| [Decisiones (ADRs)](decisiones/0001-por-que-k3d.md) | Por qué k3d, por qué Argo CD, por qué HTTP/2 sobre QUIC, etc. |
|
||||||
|
| [Playbooks](playbooks/incidente-crashloop-medusa.md) | Incidentes reales, diagnóstico paso a paso, causa raíz, fix |
|
||||||
|
| [Aprendizajes](aprendizajes/notas-sueltas.md) | Qué haría distinto, gotchas de Medusa v2 |
|
||||||
|
| [Guía del estudiante](guia-estudiante/README.md) | Ruta de aprendizaje desde cero |
|
||||||
|
|
||||||
|
## Estado de este sitio
|
||||||
|
|
||||||
|
Este portal se mantiene vivo junto con el lab — cada página muestra su
|
||||||
|
última fecha de modificación real (`git-revision-date-localized`). Si
|
||||||
|
una página dice "TODO", es contenido pendiente de una fase posterior,
|
||||||
|
no una promesa incumplida: la estructura completa se construyó primero
|
||||||
|
a propósito, para que el contenido se llene sección por sección con el
|
||||||
|
mismo criterio que el resto del lab.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# Incidente: 504 Gateway Timeout intermitente (túnel QUIC inestable) (2026-08)
|
||||||
|
|
||||||
|
!!! info "Origen"
|
||||||
|
Migrado desde `apps-registry/docs/playbooks/incidente-cloudflared-504-quic-2026-08.md`.
|
||||||
|
Contenido técnico preservado sin cambios de fondo — solo formato adaptado a MkDocs Material.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | Intermitente durante varias horas, 2026-08 |
|
||||||
|
| **Servicio afectado** | `cloudflared` (túnel de Cloudflare, app nativa de ZimaOS, red Host) — afecta a **todos** los subdominios detrás del mismo túnel (`gitea.cruzcloud.net`, `commerce.cruzcloud.net`, `shop.cruzcloud.net`, `media.cruzcloud.net`, Immich, Argo CD, Memos, etc.) |
|
||||||
|
| **Impacto** | `504 Gateway Timeout` intermitente en peticiones HTTP a cualquier host detrás del túnel, sin patrón evidente de horario ni de carga |
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
`gitea.cruzcloud.net` devolvía `504 Gateway Timeout` de forma
|
||||||
|
intermitente. El mismo síntoma se observaba en otros hosts servidos por
|
||||||
|
el mismo túnel, lo que apuntaba a una causa **compartida a nivel de
|
||||||
|
túnel**, no de una app individual.
|
||||||
|
|
||||||
|
## Diagnóstico
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -f cloudflared 2>&1 | grep -i -E "connIndex|protocol|retry"
|
||||||
|
```
|
||||||
|
|
||||||
|
Reveló un patrón sostenido durante horas: la conexión `connIndex=2`
|
||||||
|
(de las 4 conexiones que `cloudflared` mantiene hacia el edge de
|
||||||
|
Cloudflare) sufría fallos recurrentes de "control stream" cada 2-6
|
||||||
|
minutos, reconectándose en loop:
|
||||||
|
|
||||||
|
```text
|
||||||
|
control stream encountered a failure while serving
|
||||||
|
Retrying connection in up to 1m4s
|
||||||
|
```
|
||||||
|
|
||||||
|
Cuando una petición HTTP coincidía con el instante exacto de una de
|
||||||
|
esas caídas de `connIndex=2`, el cliente recibía el 504.
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
El contenedor `cloudflared` (imagen oficial `cloudflare/cloudflared:latest`)
|
||||||
|
usaba **QUIC (UDP)** como protocolo de transporte por defecto para sus
|
||||||
|
4 conexiones hacia el edge de Cloudflare. Una de esas conexiones
|
||||||
|
presentaba fallos de control stream recurrentes durante horas.
|
||||||
|
|
||||||
|
!!! warning "Causa de fondo probable"
|
||||||
|
Inestabilidad de UDP/QUIC en la red local (NAT/firewall doméstico).
|
||||||
|
QUIC es más sensible que TCP/HTTP2 a NATs agresivos o middleboxes
|
||||||
|
que no manejan bien tráfico UDP de larga vida — un patrón común en
|
||||||
|
redes domésticas, a diferencia de datacenters con NAT dedicado para
|
||||||
|
este tipo de tráfico.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Se agregó la variable de entorno `TUNNEL_TRANSPORT_PROTOCOL=http2` en
|
||||||
|
la configuración del contenedor `cloudflared` (app ZimaOS → Ambiente →
|
||||||
|
variables), forzando HTTP/2 sobre TCP en vez de QUIC/UDP.
|
||||||
|
|
||||||
|
Tras reiniciar el contenedor, el precheck de `cloudflared` confirmó:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Environment is healthy. cloudflared will use 'http2' as primary protocol.
|
||||||
|
```
|
||||||
|
|
||||||
|
y las 4 conexiones pasaron a `protocol=http2`.
|
||||||
|
|
||||||
|
Ver también el ADR correspondiente:
|
||||||
|
[0003 — Por qué HTTP/2 sobre QUIC](../decisiones/0003-por-que-http2-sobre-quic-tunnel.md).
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Curl en loop contra `gitea.cruzcloud.net` tras el cambio: latencia
|
||||||
|
estable ~0.37–0.4s sostenida por 24+ minutos sin un solo 504.
|
||||||
|
|
||||||
|
!!! note "Degradación puntual no relacionada, pendiente de vigilar"
|
||||||
|
Durante la ventana de validación se observó una ventana breve de
|
||||||
|
latencia alta (hasta 39s) y algunos `502`/`530`, coincidiendo con
|
||||||
|
una ejecución pesada de Gitea Actions (build/deploy de Medusa) en
|
||||||
|
el mismo host. Esto es contención de recursos local (CPU/IO del
|
||||||
|
host compitiendo entre el runner de Actions y el resto de los
|
||||||
|
servicios), **no relacionado al fix de protocolo del túnel** — no
|
||||||
|
se reabrió como parte de este incidente.
|
||||||
|
|
||||||
|
**Pendiente:** vigilar si estas ventanas de degradación coinciden
|
||||||
|
sistemáticamente con builds/deploys de Gitea Actions. Si es
|
||||||
|
recurrente, considerar mover el runner (`gitea-runner`) a otro host
|
||||||
|
o limitarle recursos para evitar que compita con el resto de las
|
||||||
|
apps del NAS.
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Incidente: imágenes de producto en blanco (proxy host de `media.cruzcloud.net` mal configurado en NPM) (2026-08)
|
||||||
|
|
||||||
|
!!! info "Origen"
|
||||||
|
Migrado desde `apps-registry/docs/playbooks/incidente-medusa-imagenes-proxy-npm-2026-08.md`.
|
||||||
|
Contenido técnico preservado sin cambios de fondo — solo formato adaptado a MkDocs Material.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-13, resuelto el mismo día |
|
||||||
|
| **Servicio afectado** | `media.cruzcloud.net` (Nginx Proxy Manager → MinIO, `minio-svc` en namespace `ecommerce`) |
|
||||||
|
| **Impacto** | Las imágenes subidas a productos desde el Admin de Medusa no se visualizaban en el detalle de producto del storefront (`shop.cruzcloud.net`) — el resto del producto (título, precio, subtítulo, botón añadir al carrito) se renderizaba bien |
|
||||||
|
|
||||||
|
## Síntoma
|
||||||
|
|
||||||
|
Al agregar imágenes a un producto desde el Admin de Medusa (sección
|
||||||
|
Media), estas se veían correctamente cargadas en el Admin, pero en la
|
||||||
|
página de detalle de producto del frontend los thumbnails aparecían
|
||||||
|
vacíos/en blanco.
|
||||||
|
|
||||||
|
## Diagnóstico
|
||||||
|
|
||||||
|
1. **Storage:** confirmado que Medusa usa el provider
|
||||||
|
`@medusajs/medusa/file-s3` apuntando a MinIO interno
|
||||||
|
(`S3_ENDPOINT=http://minio-svc:9000`, bucket `ari-products`), con
|
||||||
|
URLs públicas construidas sobre
|
||||||
|
`S3_FILE_URL=https://media.cruzcloud.net/ari-products` (env en
|
||||||
|
configmap `commerce-config`, namespace `ecommerce`).
|
||||||
|
2. **API:** el producto traía URLs absolutas válidas, ej.
|
||||||
|
`https://media.cruzcloud.net/ari-products/<archivo>.jpg`.
|
||||||
|
3. **Prueba directa de la URL:**
|
||||||
|
- Contra el ingress interno del cluster (bypaseando el proxy
|
||||||
|
externo, `Host: media.cruzcloud.net` directo a la IP del
|
||||||
|
`k3d-proxy`) → `200 OK`, MinIO sirve el JPEG correctamente.
|
||||||
|
- Contra `https://media.cruzcloud.net/...` (la ruta real que usa el
|
||||||
|
navegador del cliente, vía Cloudflare) → `500 Internal Server
|
||||||
|
Error`, `Server: openresty`.
|
||||||
|
|
||||||
|
!!! tip "Patrón de diagnóstico clave"
|
||||||
|
Ese contraste (funciona interno, falla público, con
|
||||||
|
`Server: openresty` — el motor de Nginx Proxy Manager) aisló el
|
||||||
|
problema al **proxy externo**, no a Medusa/MinIO/frontend.
|
||||||
|
|
||||||
|
## Causa raíz
|
||||||
|
|
||||||
|
En Nginx Proxy Manager (`/DATA/AppData/nginxproxymanager/`, contenedor
|
||||||
|
`nginxproxymanager`), el proxy host de `media.cruzcloud.net`
|
||||||
|
(`id=13` en la tabla `proxy_host` de `database.sqlite`) estaba mal
|
||||||
|
configurado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
forward_host = "http://192.168.68.61/" -- esquema y barra final metidos dentro del campo (formato inválido)
|
||||||
|
forward_port = 5005 -- puerto donde no escucha ningún proceso en el host
|
||||||
|
```
|
||||||
|
|
||||||
|
Los proxy hosts que sí funcionan (`shop.cruzcloud.net` id 23,
|
||||||
|
`commerce.cruzcloud.net` id 28) apuntan correctamente a:
|
||||||
|
|
||||||
|
```text
|
||||||
|
forward_host = "192.168.68.61"
|
||||||
|
forward_port = 90 -- puerto publicado por k3d-lab-cluster-serverlb hacia Traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
Se confirmó con `ss`/`docker ps` en el host que nada escuchaba en el
|
||||||
|
puerto `5005` — este proxy host nunca estuvo apuntando correctamente
|
||||||
|
al cluster, muy probablemente un typo desde que se creó.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
1. Backups previos: `database.sqlite.bak-pre-media-fix` y
|
||||||
|
`13.conf.bak-pre-media-fix`.
|
||||||
|
2. Corrección directa en la base SQLite de NPM:
|
||||||
|
```sql
|
||||||
|
UPDATE proxy_host SET forward_host='192.168.68.61', forward_port=90 WHERE id=13;
|
||||||
|
```
|
||||||
|
3. NPM regeneró automáticamente `nginx/proxy_host/13.conf` con los
|
||||||
|
valores correctos (la app Node de NPM detecta el cambio en la base
|
||||||
|
y reescribe el `.conf`; no hace falta editarlo a mano).
|
||||||
|
4. Recarga de nginx dentro del contenedor:
|
||||||
|
```bash
|
||||||
|
docker exec nginxproxymanager nginx -t
|
||||||
|
docker exec nginxproxymanager nginx -s reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicionalmente, se seteó el campo `thumbnail` del producto de prueba
|
||||||
|
desde el Admin (estaba en `null`) — Medusa no lo autocompleta a partir
|
||||||
|
del array `images`, y el frontend lo usa como imagen hero en listados/
|
||||||
|
tarjetas de catálogo.
|
||||||
|
|
||||||
|
## Validación
|
||||||
|
|
||||||
|
Confirmado visualmente en
|
||||||
|
`shop.cruzcloud.net/product/cybertron-sentinel-figura-de-coleccion-test-revalidate`
|
||||||
|
que tanto la galería del detalle como el thumbnail en listados/tarjetas
|
||||||
|
se renderizan correctamente. También se validó que ediciones de
|
||||||
|
descripción/título/precio desde el Admin se reflejan automáticamente
|
||||||
|
en el storefront sin necesidad de restart ni intervención manual, vía
|
||||||
|
el subscriber `revalidate-storefront.ts`
|
||||||
|
(`workloads/commerce-backend/src/subscribers/`) que procesa el evento
|
||||||
|
`product.updated` de Medusa.
|
||||||
|
|
||||||
|
## Lecciones aprendidas
|
||||||
|
|
||||||
|
**Aislar interno vs. público antes de sospechar de la app.** Medusa,
|
||||||
|
MinIO y el frontend estaban sanos todo el tiempo — la falla estaba en
|
||||||
|
una capa de infraestructura fuera de los repos de GitOps (Nginx Proxy
|
||||||
|
Manager, gestionado por fuera de `apps-registry`/`platform-infra`, sin
|
||||||
|
versionar). Comparar la misma petición por la ruta interna del cluster
|
||||||
|
contra la ruta pública real fue lo que aisló la causa en minutos, sin
|
||||||
|
necesidad de tocar Medusa ni el frontend.
|
||||||
|
|
||||||
|
Ver [Red y exposición](../arquitectura/red-y-exposicion.md) para la
|
||||||
|
topología completa Cloudflare Tunnel → Nginx Proxy Manager → k3d y el
|
||||||
|
patrón a seguir si otro subdominio (`*.cruzcloud.net`) presenta el
|
||||||
|
mismo síntoma (`500`/`Server: openresty` público pero sano
|
||||||
|
internamente).
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# Incidente: precios `null` en detalle de producto (Medusa Store API sin `region_id`) (2026-08)
|
||||||
|
|
||||||
|
!!! info "Origen"
|
||||||
|
Migrado desde `apps-registry/docs/playbooks/incidente-medusa-precios-region-id-2026-08.md`.
|
||||||
|
Contenido técnico preservado sin cambios de fondo — solo formato adaptado a MkDocs Material.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| **Ventana del incidente** | 2026-08-12, resuelto el mismo día (commit `60072cf`) |
|
||||||
|
| **Servicio afectado** | `medusa-svc` (Store API), consumido por el frontend Next.js de ARI Shopping |
|
||||||
|
| **Impacto** | En el listado de productos (`/catalog`) los precios se mostraban correctamente, pero en la página de detalle de producto individual varios artículos mostraban "Consultar precio" en vez del monto real |
|
||||||
|
|
||||||
|
## Hipótesis inicial descartada: regiones COP duplicadas
|
||||||
|
|
||||||
|
La primera hipótesis fue que existían regiones "Colombia (COP)"
|
||||||
|
duplicadas en Medusa, causando ambigüedad al resolver el precio. Se
|
||||||
|
descartó con evidencia directa en Postgres, consultando la tabla
|
||||||
|
`region` **incluyendo soft-deletes**:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT id, name, currency_code, created_at, updated_at, deleted_at FROM region ORDER BY created_at;
|
||||||
|
|
||||||
|
id | name | currency_code | created_at | updated_at | deleted_at
|
||||||
|
----------------------------------+----------+---------------+----------------------------+----------------------------+------------
|
||||||
|
reg_01KZT86WPAH7A7ZZRDSX3350V2 | Colombia | cop | 2026-08-12 05:48:03.151+00 | 2026-08-12 05:48:03.151+00 |
|
||||||
|
(1 row)
|
||||||
|
```
|
||||||
|
|
||||||
|
Una sola fila, creada una sola vez, nunca actualizada, nunca borrada.
|
||||||
|
**No había ni hubo nunca una región duplicada** — la hipótesis de
|
||||||
|
deduplicación quedó descartada con esta consulta.
|
||||||
|
|
||||||
|
## Causa raíz real
|
||||||
|
|
||||||
|
El backend de Medusa **no tenía ninguna región configurada**. El
|
||||||
|
frontend pedía precios al Store API (`/store/products`) usando el
|
||||||
|
parámetro `currency_code=cop`, pero el validador
|
||||||
|
`StoreGetProductsParams` de Medusa v2 (`.strict()`) **no acepta ese
|
||||||
|
campo** — devolvía `400 Unrecognized fields: 'currency_code'` en cada
|
||||||
|
carga de `/catalog`.
|
||||||
|
|
||||||
|
Se probó también `country_code`, que tampoco funcionó — Medusa
|
||||||
|
respondía:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Missing required pricing context to calculate prices - region_id
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! danger "Nota de arquitectura"
|
||||||
|
**Medusa v2 requiere explícitamente un `region_id` válido** para
|
||||||
|
poder calcular precios (`calculated_price`) vía Store API; ni
|
||||||
|
`currency_code` ni `country_code` son parámetros válidos para ese
|
||||||
|
fin.
|
||||||
|
|
||||||
|
## Fix aplicado
|
||||||
|
|
||||||
|
Commit `60072cf` — *"fix: usar region_id en vez de currency_code para
|
||||||
|
precios de Medusa"* (PR #5, mergeado en `04dc6bd`):
|
||||||
|
|
||||||
|
1. Se creó la región "Colombia" (moneda COP) vía Admin API — no
|
||||||
|
existía ninguna región configurada previamente.
|
||||||
|
2. Se modificó el frontend para pedir precios usando `region_id`
|
||||||
|
explícito en vez de `currency_code`, vía una nueva variable de
|
||||||
|
entorno `MEDUSA_REGION_ID`:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
# workloads/ecommerce/frontend.yaml
|
||||||
|
- name: MEDUSA_BACKEND_URL
|
||||||
|
value: http://medusa-svc:9000
|
||||||
|
+ - name: MEDUSA_REGION_ID
|
||||||
|
+ value: reg_01KZT86WPAH7A7ZZRDSX3350V2
|
||||||
|
```
|
||||||
|
|
||||||
|
```diff
|
||||||
|
# workloads/ecommerce/lib/headless/providers/medusa.ts
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
limit: String(limit),
|
||||||
|
- currency_code: "cop",
|
||||||
|
+ // El validador de /store/products no acepta currency_code (400
|
||||||
|
+ // "Unrecognized fields"); el precio calculado solo se resuelve con
|
||||||
|
+ // region_id.
|
||||||
|
+ region_id: regionId(),
|
||||||
|
```
|
||||||
|
|
||||||
|
No hubo migración de precios entre regiones — no existían datos
|
||||||
|
previos que migrar; la región se creó de cero como parte de este mismo
|
||||||
|
fix. `MEDUSA_REGION_ID` no ha cambiado desde el commit original
|
||||||
|
(verificado con `git log -p --follow` sobre `frontend.yaml`), y sigue
|
||||||
|
apuntando a la única región que existe en la base.
|
||||||
|
|
||||||
|
## Lecciones aprendidas
|
||||||
|
|
||||||
|
**Verificar la causa raíz contra el estado real de la base de datos
|
||||||
|
antes de asumir la primera hipótesis**, aun cuando esa hipótesis
|
||||||
|
coincida con la intuición inicial de quien reporta el bug ("puede que
|
||||||
|
haya algo duplicado"). La consulta a `region` incluyendo `deleted_at`
|
||||||
|
tomó segundos y evitó documentar una causa raíz incorrecta en este
|
||||||
|
mismo playbook.
|
||||||
|
|
||||||
|
**Nota de arquitectura:** Medusa v2 Store API (`StoreGetProductsParams`)
|
||||||
|
requiere `region_id` explícito para resolver precios calculados.
|
||||||
|
`currency_code` y `country_code` no son parámetros válidos para ese
|
||||||
|
fin.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: docs-portal-ingress
|
||||||
|
annotations:
|
||||||
|
traefik.ingress.kubernetes.io/router.entrypoints: web
|
||||||
|
spec:
|
||||||
|
ingressClassName: traefik
|
||||||
|
rules:
|
||||||
|
- host: docs.cruzcloud.net
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: docs-portal-svc
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: docs-portal
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
site_name: CruzCloud Lab
|
||||||
|
site_description: >-
|
||||||
|
Portal de documentación del lab GitOps CruzCloud — arquitectura, decisiones
|
||||||
|
y playbooks de incidentes reales (ZimaOS + k3d + Gitea + Argo CD).
|
||||||
|
site_url: https://docs.cruzcloud.net/
|
||||||
|
repo_url: https://gitea.cruzcloud.net/devops/apps-registry
|
||||||
|
repo_name: devops/apps-registry
|
||||||
|
edit_uri: _blank
|
||||||
|
|
||||||
|
docs_dir: docs
|
||||||
|
|
||||||
|
theme:
|
||||||
|
name: material
|
||||||
|
language: es
|
||||||
|
palette:
|
||||||
|
- media: "(prefers-color-scheme: light)"
|
||||||
|
scheme: default
|
||||||
|
primary: indigo
|
||||||
|
accent: indigo
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-night
|
||||||
|
name: Cambiar a modo oscuro
|
||||||
|
- media: "(prefers-color-scheme: dark)"
|
||||||
|
scheme: slate
|
||||||
|
primary: indigo
|
||||||
|
accent: indigo
|
||||||
|
toggle:
|
||||||
|
icon: material/weather-sunny
|
||||||
|
name: Cambiar a modo claro
|
||||||
|
features:
|
||||||
|
- navigation.tabs
|
||||||
|
- navigation.tabs.sticky
|
||||||
|
- navigation.sections
|
||||||
|
- navigation.top
|
||||||
|
- navigation.footer
|
||||||
|
- navigation.indexes
|
||||||
|
- search.suggest
|
||||||
|
- search.highlight
|
||||||
|
- content.code.copy
|
||||||
|
- content.tabs.link
|
||||||
|
- toc.follow
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- search:
|
||||||
|
lang: es
|
||||||
|
- git-revision-date-localized:
|
||||||
|
enable_creation_date: true
|
||||||
|
type: date
|
||||||
|
fallback_to_build_date: true
|
||||||
|
|
||||||
|
markdown_extensions:
|
||||||
|
- admonition
|
||||||
|
- attr_list
|
||||||
|
- md_in_html
|
||||||
|
- tables
|
||||||
|
- toc:
|
||||||
|
permalink: true
|
||||||
|
- pymdownx.details
|
||||||
|
- pymdownx.superfences:
|
||||||
|
custom_fences:
|
||||||
|
- name: mermaid
|
||||||
|
class: mermaid
|
||||||
|
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||||
|
- pymdownx.highlight:
|
||||||
|
anchor_linenums: true
|
||||||
|
- pymdownx.inlinehilite
|
||||||
|
- pymdownx.snippets
|
||||||
|
- pymdownx.tabbed:
|
||||||
|
alternate_style: true
|
||||||
|
|
||||||
|
nav:
|
||||||
|
- Inicio: index.md
|
||||||
|
- Arquitectura:
|
||||||
|
- Visión general: arquitectura/vision-general.md
|
||||||
|
- Red y exposición: arquitectura/red-y-exposicion.md
|
||||||
|
- Flujo GitOps: arquitectura/gitops-flujo.md
|
||||||
|
- Glosario: arquitectura/glosario.md
|
||||||
|
- Decisiones (ADRs):
|
||||||
|
- "0001 · Por qué k3d": decisiones/0001-por-que-k3d.md
|
||||||
|
- "0002 · Por qué Argo CD": decisiones/0002-por-que-argocd-vs-manual.md
|
||||||
|
- "0003 · HTTP/2 sobre QUIC": decisiones/0003-por-que-http2-sobre-quic-tunnel.md
|
||||||
|
- Plantilla ADR: decisiones/plantilla-adr.md
|
||||||
|
- Playbooks:
|
||||||
|
- Crashloop Medusa: playbooks/incidente-crashloop-medusa.md
|
||||||
|
- 504 túnel Gitea: playbooks/incidente-504-gitea-tunnel.md
|
||||||
|
- Precios Medusa: playbooks/incidente-precios-medusa.md
|
||||||
|
- Imágenes NPM: playbooks/incidente-imagenes-npm.md
|
||||||
|
- Aprendizajes:
|
||||||
|
- Notas sueltas: aprendizajes/notas-sueltas.md
|
||||||
|
- Guía del estudiante:
|
||||||
|
- Inicio: guia-estudiante/README.md
|
||||||
|
- Conceptos básicos: guia-estudiante/conceptos-basicos.md
|
||||||
|
|
||||||
|
extra:
|
||||||
|
social:
|
||||||
|
- icon: fontawesome/brands/git-alt
|
||||||
|
link: https://gitea.cruzcloud.net/devops
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
mkdocs==1.6.*
|
||||||
|
mkdocs-material==9.5.*
|
||||||
|
mkdocs-git-revision-date-localized-plugin==1.2.*
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: docs-portal-svc
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: docs-portal
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
Reference in New Issue
Block a user