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.
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, testsapps/loadgen/— generador de tráfico en Goapps/web/— frontend chulo, con badge de versión, panel de salud y contador de erroresbuild/— Dockerfile multi-stage + composeobservability/— 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
nereuscreado en Forgejo + espejo a GitHub configurado - ISO Fedora Server 42, dos VMs a mano con virt-manager
systemctl disable --now zram-generatoro el swap toca los huevos a k3sdnf install k3s-selinuxantes 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 anereus, 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
loadgeny 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