Nereus/PLAN.md
Fi3w0 1fbe714b6d docs: add project plan and agent ground rules
AGENTS.md carries the owner's rules for every agent: commit identity, secret
handling, surgical edits, and writing style. PLAN.md is the owner's worklog and
is off limits to agents.
2026-08-20 20:01:04 +02:00

14 KiB

Nereus — Plan del Proyecto Final DevOps

Qué es esto

Mi plan para el PF de Tokio. No es la memoria, es mi guion de trabajo. Fecha objetivo: infra + integración en 2 días, memoria y vídeo después.

La idea

Una API que registra lecturas de boyas oceánicas. Go + Postgres, frontend estático.

La app es lo de menos. El proyecto real es todo lo que la rodea: k3s en Fedora, CI/CD, blue-green con rollback automático decidido por Prometheus, y observabilidad completa. La app solo tiene que generar telemetría interesante y poder fallar a demanda.

Empresa ficticia del enunciado: TechWave Solutions. Encaja con la estética de agua, así que tiro por ahí y ya.

Arquitectura

PC principal (CachyOS, 32GB)
│
├── libvirt/KVM
│   ├── nereus-server   Fedora Server 42 · 4 vCPU · 12GB · k3s server + agent
│   └── nereus-agent    Fedora Server 42 · 4 vCPU · 10GB · k3s agent
│
└── ~10GB libres para el host

Mini PC (homelab, siempre encendido)
├── Forgejo               git.fiwlabs.dev, detrás de Traefik con auto-TLS
├── Forgejo Runner        modo docker, misma LAN que el clúster
├── Registro de imágenes  incluido en Forgejo
└── MinIO                 backend remoto de Terraform

Espejo automático a GitHub → el link que entrego a Tokio

Dentro del clúster:

Traefik (viene con k3s)
   └── Ingress → Service activo
                    ├── Rollout blue   (v1, tráfico real)
                    └── Rollout green  (v2, preview, sin tráfico)

Postgres (StatefulSet + PVC local-path)

Observabilidad:
   kube-prometheus-stack   Prometheus + Grafana + node-exporter + kube-state-metrics
   Loki + Alloy            logs
   OTel Collector → Tempo  trazas
   Alertmanager → Discord  webhook

Por qué Forgejo y no GitHub

El enunciado dice "GitHub Actions o la herramienta de tu preferencia", así que estoy cubierto. Y me interesa porque:

  • El runner ya vive en mi red, llega al k3s por LAN sin túneles ni runner self-hosted registrado contra un tercero
  • Registro de contenedores incluido, con TLS válido de Traefik → k3s hace pull sin insecure: true
  • Ecosistema DevOps entero self-hosted: forja, registro, runner, clúster. Cero dependencia de terceros

Pero: la entrega pide enlace al repositorio. Si el profe abre el link y mi homelab está caído, malo. Por eso espejo automático a GitHub (Settings → Repository → Mirror Settings, se configura una vez). Entrego el link de GitHub, el pipeline real corre en mi infra.

Como los workflows viven en .forgejo/workflows/, GitHub los ignora. No se me ejecuta nada por accidente allí.

Cosas que me van a morder

Actions de terceros. Forgejo los busca en code.forgejo.org por defecto. actions/checkout está mirrorizado, pero trivy-action o gitleaks-action probablemente no. Solución: ejecutarlos como CLI en docker, no como action. Menos dependencias y más portable:

- run: |
    docker run --rm -v $PWD:/repo zricethezav/gitleaks:latest \
      detect --source /repo --no-git -v

Si aun así necesito actions de GitHub: DEFAULT_ACTIONS_URL = github en app.ini.

El runner necesita Docker. Modo docker en la config, usuario del runner en el grupo docker. Ojo con conflictos de puertos con el resto del stack del Mini PC.

Credenciales del pipeline. El kubeconfig va como secret de repo, pero no el de admin. Creo un ServiceAccount deployer con RBAC limitado al namespace nereus y genero un kubeconfig con ese token. Eso es un punto directo de "manejo seguro de credenciales" y es respuesta preparada si preguntan.

Reparto: yo vs agentes

Yo hago todo lo que necesita el clúster vivo o hardware real:

  • VMs Fedora, k3s, firewalld, SELinux
  • Terraform (infra y platform)
  • Forgejo Runner y su acceso al clúster
  • Integración de los manifiestos, iterar hasta que aplique
  • Que el AnalysisRun aborte de verdad
  • Capturas y vídeo

Agentes hacen lo que se valida sin clúster:

  • apps/api/ — Go, endpoints, OTel, tests
  • apps/loadgen/ — generador de tráfico en Go
  • apps/web/ — frontend chulo, con badge de versión, panel de salud y contador de errores
  • build/ — Dockerfile multi-stage + compose
  • observability/ — dashboards JSON, alert rules, config del collector

Cuatro tareas en paralelo, una por carpeta, así no se pisan. Reglas en AGENTS.md.

terraform/ y deploy/ son míos, los agentes no los tocan.

Día 1

Infra primero, a mano, antes de codificarla en Terraform. Terraform a ciegas es sufrir.

Paso 0 — la clave SSH, que no la tengo en el PC principal:

ssh-keygen -t ed25519 -C "fiw@pc-principal"
cat ~/.ssh/id_ed25519.pub
# → Forgejo: Settings → SSH/GPG Keys → Add Key

ssh -T git@git.fiwlabs.dev      # verificar antes de seguir

Esa misma clave la meto en el cloud-init de las VMs (ssh_authorized_keys), así que la necesito antes de tocar Terraform. Y como el PC es dual-boot, la genero en CachyOS, que es donde va a vivir el clúster.

Si el SSH de Forgejo va por un puerto no estándar, ~/.ssh/config:

Host git.fiwlabs.dev
    User git
    Port 2222
    IdentityFile ~/.ssh/id_ed25519
  • Clave SSH generada y añadida a Forgejo
  • Repo nereus creado en Forgejo + espejo a GitHub configurado
  • ISO Fedora Server 42, dos VMs a mano con virt-manager
  • systemctl disable --now zram-generator o el swap toca los huevos a k3s
  • dnf install k3s-selinux antes de instalar k3s
  • firewalld: abrir 6443/tcp, 10250/tcp, 8472/udp
  • Instalar k3s server en nereus-server, agent en nereus-agent
  • Verificar que un pod en un nodo hace ping a un pod del otro. Si esto falla es el 8472/udp, siempre
  • Reserva de IP estática para las dos VMs en la red de libvirt
  • Snapshot de las dos VMs
  • Lanzar los 4 agentes en paralelo
  • Codificar las VMs en terraform/infra/ (libvirt + cloud-init)
  • MinIO en el Mini PC como backend de estado

Día 2

  • terraform/platform/ — helm: kube-prometheus-stack, Loki, Tempo, Argo Rollouts, Sealed Secrets
  • Manifiestos de la app con kustomize, aplicar, iterar
  • Sealed Secrets: cifrar la password de Postgres y commitearla
  • Backup de la clave privada de Sealed Secrets fuera del repo (sin esto, reencender = perder todos los secretos)
  • ServiceAccount deployer + RBAC limitado a nereus, generar su kubeconfig
  • Forgejo Runner en el Mini PC (modo docker), registrado y conectado al clúster
  • Pipeline CI: test, lint, gitleaks, trivy, build, push al registro de Forgejo
  • Pipeline CD: kustomize + promoción del Rollout, con el kubeconfig de deployer
  • Verificar que k3s hace pull del registro sin insecure: true
  • Desplegar loadgen y verificar que Prometheus ve tráfico constante
  • AnalysisTemplate consultando Prometheus, verificar que aborta
  • Ciclo de demo completo: desplegar v2 con CHAOS_ERROR_RATE=0.3, ver a Rollouts matarla sola, captura de Grafana + ping de Discord

Esa demo es la captura que vale por tres páginas de memoria. Que no se me olvide grabarla.

Trampas de Fedora Server

Nunca lo he tocado. Estas tres caen seguro, y las documento como "dificultades encontradas" que el enunciado pide literalmente:

Problema Síntoma Fix
firewalld bloquea VXLAN Pods no se ven entre nodos, DNS falla raro Abrir 8472/udp
SELinux enforcing kubelet peta con permisos dnf install k3s-selinux
zram/swap activo k3s se queja al arrancar Desactivar zram-generator

Dónde cubro cada requisito del enunciado

Pide Dónde
Docker imágenes personalizadas y optimizadas Multi-stage → distroless, ~15MB. Captura de docker images comparando
Docker Compose compose.yaml para entorno de desarrollo local
Terraform IaC terraform/infra/ (libvirt) + terraform/platform/ (helm)
Modularización y estado remoto Módulos separados, backend S3 en MinIO
Cloud (EKS/AKS) No lo hago. Justifico on-premise por coste y control. El código Terraform es portable
Deployments, Services, Ingress, ConfigMaps, Secrets deploy/base/ con kustomize
Pipeline CI/CD Forgejo Actions + runner propio. El enunciado permite "la herramienta de tu preferencia"
Registro de contenedores Registro de Forgejo, self-hosted con TLS de Traefik
Manejo seguro de secretos Sealed Secrets cifrados en el repo, Gitleaks en CI, ServiceAccount deployer con RBAC mínimo
Blue-Green Argo Rollouts, active + preview service
Rollback automático AnalysisTemplate consultando Prometheus, aborta solo
OTel Collector Trazas de la app → collector → Tempo
Prometheus kube-prometheus-stack
Grafana con dashboards personalizados observability/dashboards/
Loki Logs centralizados vía Alloy
cAdvisor y node exporter Vienen con el stack (cAdvisor del kubelet)
Alertas y notificaciones Alertmanager → webhook de Discord

El clúster es efímero, y eso es una virtud

No voy a dejar esto encendido días. Lo levanto en mi PC, grabo el vídeo showcase, capturas al README y a la entrega, y lo apago. Cuando haga falta lo enciendo otra vez.

Esto no es una carencia del proyecto, es la prueba de que el IaC funciona. Si el clúster se reconstruye entero desde el repositorio con un comando, es que Terraform y los manifiestos son la fuente de verdad de verdad. Lo escribo así en la memoria.

Tres niveles de reproducibilidad

El profe no va a montar KVM ni a bajarse Fedora. Necesito niveles:

Nivel Comando Requisitos Qué demuestra
1 docker compose up Docker, 4GB App + Postgres + Grafana. 3 minutos, cualquier OS
2 ./scripts/up-k3d.sh Docker, 8GB k3s real multi-nodo en contenedores: manifiestos, Argo Rollouts, blue-green y rollback completos. Funciona en Windows con Docker Desktop
3 terraform apply KVM, 24GB Mi clúster real de 2 nodos Fedora

El nivel 2 es el que importa para la corrección. k3d mete un k3s multi-nodo dentro de Docker, así que el proyecto entero corre ahí sin mentir sobre nada. Y me sirve a mí para iterar rápido sin arrancar VMs.

En el README, el nivel 2 va arriba del todo y bien visible.

⚠️ Sealed Secrets y el clúster efímero

Si borro el clúster, pierdo la clave privada. El controlador genera un par nuevo al reinstalarse y todos mis *-sealed.yaml commiteados quedan imposibles de descifrar para siempre.

Lo primero al montar Sealed Secrets:

kubectl get secret -n kube-system \
  -l sealedsecrets.bitnami.com/sealed-secrets-key \
  -o yaml > ~/backups/sealed-secrets-key.yaml    # NUNCA al repo

up.sh restaura esa clave antes de aplicar nada. Si no, cada reencendido me obliga a re-sellar todos los secretos a mano.

Otras cosas que se rompen al reencender

  • IPs de las VMs — DHCP me da otra y el kubeconfig apunta a la vieja. Reserva estática en la red de libvirt desde el día 1
  • Datos de Prometheus — se pierden si el PVC es efímero. Da igual, el loadgen repuebla los dashboards en 5 minutos
  • Orden de arranque — si el agent arranca antes que el server, el join falla. El script espera a que el server responda

Scripts

scripts/
├── up.sh              arranca VMs, espera a k3s, restaura la clave, aplica todo
├── down.sh            apaga limpio
├── up-k3d.sh          nivel 2, para el profe
└── demo-rollback.sh   dispara la demo entera

demo-rollback.sh despliega v2 con chaos activado y yo solo grabo. Puedo repetir tomas hasta que salga bien sin tocar nada a mano. Encadenar comandos en vivo durante 15 minutos de vídeo es sufrimiento innecesario.

Regla de oro

Grabo el vídeo y hago TODAS las capturas mientras funciona. No lo dejo para después de apagar. Si al reencender algo se rompe, y algo se romperá, ya tengo el material.

Lo que sí es mío del todo

El profe hace tres preguntas y grabo vídeo de máximo 15 min defendiendo. Ahí no hay agente que valga.

Las que caen casi seguro:

  • ¿Por qué distroless y no alpine? → superficie de ataque, sin shell, sin gestor de paquetes
  • ¿Cómo decide el sistema que un despliegue es malo? → esta es la buena, AnalysisTemplate + Prometheus
  • ¿Cómo evitas que un secreto acabe en el repo? → Sealed Secrets + Gitleaks
  • ¿Por qué k3s y no k8s completo? → recursos, mismo API, Traefik incluido
  • ¿Por qué Forgejo y no GitHub? → runner en la misma red que el clúster, registro propio, sin dependencia de terceros. Y el espejo garantiza que el repo sea accesible igual
  • ¿Cómo reproduzco tu entorno? → tres niveles, y el 2 corre en Docker en cualquier OS. El clúster es efímero a propósito, se reconstruye desde el repo

Voy apuntando el por qué de cada decisión en docs/decisiones.md según integro. Con eso el vídeo y la memoria se escriben casi solos.

Estructura del repo

nereus/
├── AGENTS.md
├── PLAN.md
├── compose.yaml
├── apps/
│   ├── api/                  Go
│   ├── loadgen/              generador de tráfico
│   └── web/                  frontend
├── build/
│   ├── Dockerfile.api
│   ├── Dockerfile.loadgen
│   └── Dockerfile.web
├── deploy/
│   ├── base/                 kustomize
│   ├── overlays/{dev,prod}/
│   ├── rollouts/             Rollout + AnalysisTemplate
│   └── secrets/              SealedSecrets (cifrados)
├── terraform/
│   ├── infra/                libvirt + cloud-init + k3s
│   └── platform/             helm releases
├── observability/
│   ├── dashboards/
│   ├── alerts/
│   └── otel-collector/
├── .forgejo/workflows/
├── scripts/
│   ├── up.sh
│   ├── down.sh
│   ├── up-k3d.sh
│   └── demo-rollback.sh
└── docs/
    ├── decisiones.md
    └── memoria/

Comandos que voy a repetir mil veces

# kubeconfig desde el server
scp fiw@nereus-server:/etc/rancher/k3s/k3s.yaml ~/.kube/nereus
# cambiar 127.0.0.1 por la IP de nereus-server

kubectl argo rollouts get rollout nereus-api -n nereus --watch
kubectl argo rollouts promote nereus-api -n nereus
kubectl argo rollouts abort nereus-api -n nereus

kubectl port-forward -n observability svc/grafana 3000:80