# 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: ```yaml - 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:** ```bash 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: ```bash 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 ```bash # 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 ```