Mantenerlo en marcha es un trabajo en sí mismo.
Montarlo una vez fue la parte fácil. Esto es lo que me ha llevado tres años: cómo se despliega un cambio, cómo me entero de que algo va mal antes que un cliente y qué sigo haciendo a mano.
Desplegar un cambio
Cada push a un repositorio que despliego pasa por los mismos pasos: build, tests, empaquetado, despliegue y verificación. Jenkins lo ejecuta todo. No lo elegí tras comparar herramientas; ya lo había usado y sabía operarlo, y a veces ese es el motivo correcto. Harbor está a su lado como mi propio registro de imágenes, así que un build nunca depende de los límites ni de la disponibilidad de Docker Hub.
Los builds no se ejecutan en el host de Jenkins. Dos agentes en la VLAN de CI hacen el trabajo, uno en pve01 y otro en pve03. Pueden descargar dependencias y subir imágenes, y no pueden administrar nada. El razonamiento de esa separación está en /system.
Todo lo que se despliega así debería tener ya un target en Prometheus, un monitor en Uptime Kuma y, idealmente, un dashboard en Grafana antes de salir. De eso va la siguiente sección.
Esta web se despliega igual, con un pipeline que publica dos tipos de artefacto distintos desde el mismo monorepo:
git push → webhook de GitHub → Jenkins
→ docs: mkdocs build --strict → rsync al proxy interno
→ web: build de Docker → push a Harbor → despliegue con Compose en el host público
→ health check de los dos proyectos
→ un mensaje de Telegram por proyecto, o uno si falla
→ el workspace del agente se limpia siempreLas decisiones que hay detrás:
- La documentación se despliega con rsync, no como contenedor. La web de documentación es HTML estático generado con mkdocs --strict, así que un enlace roto hace fallar el build. Empaquetarla como imagen añadiría un viaje por Harbor para nada: Caddy ya sirve los ficheros directamente, así que Jenkins los genera y los copia con un usuario de despliegue que solo puede escribir en ese directorio.
- La web se despliega como imagen. Se construye desde apps/webpage, se sube a Harbor etiquetada con el commit corto y el número de build, y se vuelve a desplegar con Docker Compose en el host público.
- Los dos proyectos se despliegan en cada push. Antes detectaba qué rutas habían cambiado. Fallaba en los primeros builds y en los replays de Jenkins, de formas fáciles de pasar por alto. Los dos builds son pequeños, así que reconstruirlo todo cada vez es más sencillo y más seguro.
- Telegram es el único canal de avisos. Un mensaje por proyecto desplegado si todo va bien, y un mensaje con la etapa que falló, el commit y un enlace a la consola si algo falla. Sin email y sin un dashboard que tenga que acordarme de mirar.
- El workspace se limpia siempre. cleanWs() se ejecuta en post { cleanup {} } sea cual sea el resultado, así que un build fallido nunca deja ficheros a medias con los que tropiece el siguiente.
Vigilarlo en marcha
Durante años, monitorizar significaba abrir dashboards y cruzar los dedos. Ahora está dividido según la pregunta que responde cada herramienta:
| Señal | Responde a | Herramientas |
|---|---|---|
| Métricas | Salud de hosts, contenedores, Proxmox, bases de datos y aplicaciones | Prometheus + exporters |
| Logs | Qué ha pasado en un host o servicio | Grafana Alloy + Loki |
| Disponibilidad | Si los endpoints importantes responden, vistos desde fuera | Uptime Kuma |
| Dashboards | Consultar y visualizar métricas y logs | Grafana |
| Alertas | Avisarme antes de que lo note un cliente | Alertmanager + Telegram, Uptime Kuma + Telegram |
Cinco componentes, cada uno con una sola tarea:
| Componente | Función |
|---|---|
| Prometheus | Recoge métricas y evalúa las reglas de alerta |
| Loki | Guarda los logs que envía Grafana Alloy |
| Alertmanager | Envía las alertas de Prometheus a Telegram |
| Grafana | Dashboards sobre Prometheus y Loki |
| Uptime Kuma | Comprobaciones activas, independientes de las métricas |
Prometheus recoge métricas cada 15 segundos. Los targets que cambian según el host o la VLAN salen de ficheros JSON con file_sd_configs, uno por zona, así que añadir una máquina es editar un fichero de targets y no la configuración principal. Además de node_exporter, recoge cAdvisor en los hosts Docker (descartando algunas series de alta cardinalidad antes de guardarlas para que el número de series no se dispare), pve_exporter para el clúster, postgres_exporter para las dos bases de datos y las métricas nativas de Authentik, Zitadel y Grafana.
Grafana Alloy envía los logs de cada host a Loki, que los guarda 30 días en almacenamiento local con el esquema TSDB. El ruler de Loki ya apunta al mismo Alertmanager, así que las alertas basadas en logs usarán el mismo camino cuando las escriba. Ahora mismo no hay ninguna.
Alertmanager y Uptime Kuma avisan los dos por Telegram, pero responden a preguntas distintas. Alertmanager salta con lo que dicen las métricas: CPU alta, un target que ha dejado de responder, una base de datos sin conexiones libres. Las alertas críticas esperan menos y se repiten antes que los avisos, y los avisos de un host se silencian mientras haya una alerta crítica activa para él, así que un fallo no me manda cinco mensajes. Uptime Kuma ignora por completo las métricas y solo pregunta si un endpoint responde. Quiero las dos: una detecta la degradación interna lenta y la otra detecta el "está caído y punto" aunque todas las métricas internas parezcan bien.
Unas pocas reglas evitan que el stack se descontrole al crecer:
- Cada host nuevo lleva node_exporter y Grafana Alloy. Sin excepciones. Es la base antes de instalar cualquier otra cosa.
- cAdvisor solo donde hay Docker. No tiene sentido recoger métricas de contenedores en un host sin contenedores.
- Uptime Kuma solo donde la disponibilidad le importa a alguien. No todos los servicios internos necesitan una comprobación desde fuera, solo los que usan personas o clientes.
- Una alerta solo existe si alguien debería actuar. Una alerta sin una acción detrás es ruido, y el ruido acaba silenciado a la semana.
- Las reglas del firewall siguen los caminos de recogida de métricas. Las métricas no tienen atajo. Que Prometheus llegue al exporter de una base de datos sigue siendo una regla explícita y acotada.
Hosts coherentes
Tres máquinas con tres historias distintas (ver /system) no acaban con los mismos hábitos por sí solas. Si las dejo, cada host tiene usuarios distintos, una configuración de SSH distinta y una monitorización distinta según cuándo lo monté. No quería que eso fuera una variable, así que cada VM o LXC nueva pasa por la misma secuencia antes de hacer nada: un usuario administrador que no es root, SSH solo con clave, sin login de root ni contraseñas, y los agentes de observabilidad ya reportando antes de darla por terminada.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication noEl último paso, node_exporter y Grafana Alloy en cada host, es lo que hace fiable la monitorización de arriba. Si una máquina no reporta métricas un intervalo de recogida después de arrancar, lo trato como un error de configuración, no como algo para limpiar más adelante. Un detalle del runbook que merece la pena repetir: probar el login con clave desde una segunda terminal antes de cerrar la primera, o puedes quedarte fuera.
$ adduser alberto && usermod -aG sudo alberto
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub alberto@<new-host>
$ ssh alberto@<new-host> # segunda terminal, antes de cerrar la primera
$ sudoedit /etc/ssh/sshd_config && sudo systemctl restart sshd
$ sudo systemctl enable --now node_exporter alloy
$ curl -s localhost:9100/metrics | head -n 3Reiniciar un nodo Proxmox por una actualización de kernel o por trabajo de hardware sigue la misma disciplina al revés. Primero lo vacío: migro en vivo las VM que pueden moverse, apago con cuidado lo que no puede y compruebo que no queda nada corriendo. Después reinicio y miro cómo vuelve al clúster. Siempre un nodo cada vez, porque el clúster necesita dos de tres nodos sanos para mantener el quórum, y el siguiente no empieza hasta que pvecm status muestra el anterior completamente de vuelta. pve01 tiene una trampa extra: su kernel está fijado, porque un kernel nuevo arrancaría sin el driver de NVIDIA que necesita la GPU.
# en pve02
$ qm list # VM que siguen aquí
$ qm migrate 105 pve03 --online
$ pct list # contenedores
$ pct shutdown 210 # almacenamiento local, se para limpio
$ qm list && pct list # nada corriendo: pve02 está libre
$ reboot
...
$ pvecm status | grep Quorate
Quorate: YesPoner algo nuevo en marcha
Cada servicio que entra en la plataforma pasa por la misma lista, para que nada se aleje del modelo de red, proxies y monitorización descrito en /system:
Servicio definido → VLAN elegida → dirección y DNS asignados
→ ruta de proxy/firewall revisada → observabilidad añadida
→ riesgo de persistencia anotado → documentación actualizadaLas dos decisiones que importan son en qué VLAN cae y cuál de los tres proxies lo publica. Las dos se deducen de lo que es el servicio:
| Tipo de servicio | Ubicación por defecto |
|---|---|
| Aplicación o herramienta interna | Internal |
| Base de datos o almacenamiento de objetos | Data |
| Agente de build o automatización de CI | CI |
| Entrada desde internet | Public, normalmente solo como entrada del proxy |
| Interfaz de administración o del plano de control | Mgmt, detrás del proxy de gestión |
Todo lo demás es mecánico: la regla de firewall más estrecha que encaje con el camino real (nunca un permiso amplio de VLAN a VLAN), el firewall del host, los agentes de observabilidad, un monitor en Uptime Kuma si lo usa gente y, si guarda algo, una nota escrita de que sus datos todavía no tienen copia de seguridad.
Cuando algo se rompe
Mi documentación tiene dos tipos de páginas, y las mantengo separadas a propósito. Un runbook son pasos exactos para algo cuyos comandos no dependen del criterio: reinicia esto, migra aquello. Una guía es una forma de tomar una decisión estructural en la que la respuesta correcta depende del contexto, como aislar a un cliente nuevo. Si sabes lo que hay que hacer, quieres un runbook. Si estás decidiendo qué debería pasar, quieres una guía.
La recuperación ante desastres está escrita como un proceso, no como un script, porque todavía no hay una restauración probada fuera de los nodos. Recuperar depende de lo que siga existiendo en los hosts supervivientes y de lo que esté documentado. Por orden: averiguar qué ha fallado (un servicio, una VM, un nodo entero, un disco o la red); comprobar si el clúster mantiene el quórum y si el DNS, el enrutamiento y los proxies siguen funcionando; reconstruir lo que se pueda usando la documentación como fuente de verdad; y validarlo como un despliegue nuevo, de forma que el DNS resuelva, la ruta del proxy funcione, el target de Prometheus vuelva y Uptime Kuma se ponga en verde. Lo que el incidente demuestre que faltaba, como una ruta de configuración que nadie apuntó, va después a la documentación para que el siguiente sea un poco menos improvisado.
$ pvecm status
Quorum information
------------------
Nodes: 3
Quorate: Yes
Membership information
----------------------
Nodeid Votes Name
0x00000001 1 pve01 (local)
0x00000002 1 pve02
0x00000003 1 pve03Los runbooks que tengo hoy, por área:
| Categoría | Cubre |
|---|---|
| WireGuard | Añadir un peer de acceso remoto, añadir un túnel |
| Hosts y clúster | Setup base de máquinas, mantenimiento de nodos Proxmox, el driver de la GPU de pve01, recuperación ante desastres |
| Red | Cambios en zonas DNS, poner en marcha una VLAN nueva |
| Aplicaciones | Alojamiento de webs estáticas |
Lo que sigue siendo manual
Hay dos cosas que todavía se hacen a mano, o no se hacen:
Todavía no hay copias de seguridad funcionando. En la práctica, si muere un nodo con almacenamiento solo local, o me equivoco de bucket o de base de datos, solo puedo recuperar lo que siga existiendo en otro host. Es el mismo hueco que señala la sección de almacenamiento de /system, visto desde el lado operativo.
El segundo es la automatización, o la falta de ella. El setup base de las máquinas que he descrito antes se sigue tecleando a mano en cada host nuevo, no se ejecuta con Ansible ni con OpenTofu. Es coherente porque sigo el mismo runbook cada vez, no porque una herramienta lo imponga, y esa es una garantía mucho más débil.
Uno más pequeño: pve03 todavía no tiene Wake-on-LAN configurado. El motivo es de lo más mundano: no le he conectado un monitor para cambiar la opción de la BIOS. Como de todos modos tiene un iDRAC para encenderlo en remoto, no ha corrido prisa.
Los dos grandes están en la lista. El backlog completo (destino de copias, pruebas de restauración, despliegue del SSO y los secretos, infraestructura como código) está en /roadmap.
Qué haría distinto
Tres años después, hay dos cosas claras: una que ojalá hubiera hecho antes y otra que repetiría sin pensarlo.
- Ansible y OpenTofu tendrían que haber llegado el primer día. Cuando esto eran dos máquinas, adoptar infraestructura como código no habría costado casi nada. Con el número actual de hosts y servicios, añadirlo después es una fricción real y constante, tanta que lo sigo aplazando. Es justo el hueco de automatización de arriba. Empezar pronto habría mantenido sincronizada la configuración de cada host sin tocarlos uno a uno.
- Dividir la red mereció la pena. Ocho VLAN y sus reglas de firewall hacen que cada cambio cueste más que en una red plana. Volvería a tomar la misma decisión: el aislamiento compensa el esfuerzo de mantenerlo.
Lo siguiente: /system explica qué existe (el hardware, la red y los servicios que despliega este pipeline y vigila este stack), /security cómo se controla el acceso y /roadmap es el backlog, en orden.