Homelab k3s en alta disponibilidad
Clúster Kubernetes (k3s) de seis equipos heterogéneos en casa: x86 reciclados, mini-PCs y una Raspberry Pi. Lo uso como laboratorio de plataforma, observabilidad, seguridad y automatización y aloja servicios de uso diario. Todo lo que hay aquí funciona en producción doméstica: los incidentes, las cifras y las decisiones son reales.
Panel interactivo:
index.html, una réplica del panel de inicio (Homepage) del clúster con una foto de datos reales, que es la portada de esta web. Pulsa cualquier tarjeta para ver cómo está montado.
Escaparate anonimizado. Este repositorio se genera automáticamente desde el repo privado de operación. Las IPs (rangos de documentación RFC 5737), el dominio (
example.com), los nombres de nodo y los usuarios son ficticios; el contenido, las cifras y la historia, no. Ningún fichero se edita a mano y cada publicación es un único commit sin historial.
En cifras
| Nodos | 6: 3 masters con etcd embebido + 3 workers · amd64 y arm64 |
| Kubernetes | k3s v1.34 · ~36 namespaces |
| Endpoint de la API | VIP con keepalived (VRRP) + haproxy, relevo en ~3 s |
| Almacenamiento | Longhorn (3 réplicas) y local-path |
| GitOps | Argo CD (Homepage, Ollama, Flink, Vector…) |
| Observabilidad | Prometheus/Grafana · Elasticsearch/Kibana (ECK) · Kafka (Strimzi) → Flink → Vector |
| Seguridad | Kyverno · Falco (eBPF) · Trivy Operator · Policy Reporter · SSH solo con clave |
| Automatización | Ansible + Semaphore (portal con botones) · revisión automática de PRs con IA |
Arquitectura
flowchart TB
internet((Internet)) --> cf[Cloudflare Tunnel]
lan((LAN)) --> vip[VIP 192.0.2.250:6443<br/>keepalived + haproxy]
lan --> lb[MetalLB L2]
subgraph cp[Control-plane HA · etcd 3 miembros]
direction LR
m1[master-1<br/>VRRP 150]
m2[master-2<br/>VRRP 140]
m3[master-3<br/>VRRP 130]
end
vip --> cp
subgraph wk[Workers]
direction LR
w1[worker-1]
w2[worker-2<br/>IA local]
w3[worker-3<br/>arm64]
end
cp --- wk
cf --> traefik[Traefik Ingress]
lb --> traefik
lb --> dns[Pi-hole DNS]
traefik --> apps[Jira · Confluence · n8n]
flowchart LR fb[filebeat / elastic-agent] --> k[(Kafka KRaft)] k --> fl[Flink<br/>routing job] --> k k --> v[Vector] --> es[(Elasticsearch)] --> kb[Kibana] pr[Prometheus] --> gr[Grafana] & am[Alertmanager] fa[Falco eBPF] --> fs[Falcosidekick]
Detalle de capas y decisiones en docs/arquitectura.md, y de hardware, flags de k3s y VIP en nodes/README.md.
Decisiones técnicas
- k3s con etcd embebido y 3 masters: un solo binario, poco consumo y mezcla de arquitecturas. Permite drenar y reiniciar un master sin perder la API. Migrado desde SQLite en caliente.
- VIP en una interfaz
dummy0con/32: como IP secundaria de la interfaz real, flannel la elegía como IP pública del nodo y rompía la red overlay; con/24secuestraba la ruta de toda la subred. Las dos cosas pasaron (postmortem). - Cloudflare Tunnel para publicar sin abrir puertos en el router.
- Datos con estado según su riesgo: Longhorn con 3 réplicas para lo que debe sobrevivir a un nodo; local-path solo donde el rendimiento manda, documentado y con la regla de que cambiar su
nodeSelectorpierde datos (almacenamiento). - Versiones fijadas con tag y
@sha256, publicadas hace más de dos semanas; excepciones solo por avisos de seguridad y anotadas. - Ningún secreto en git: el exportador del estado del clúster sustituye credenciales por
<REDACTED>y falla si encuentra una clave o un token.
Postmortems
Formato síntoma → causa → solución → lección, con la regla que se sacó de cada uno (docs/incidentes.md):
| Incidente | Lección |
|---|---|
Control-plane en bucle de reinicios: etcd perdiendo el líder por I/O de filebeat, una WiFi caída en --flannel-iface y un cable defectuoso |
Nada de cargas con mucho I/O sin límites en los masters; --flannel-iface siempre en una interfaz estable |
| La VIP de keepalived rompe la red overlay de flannel | IP flotante en dummy0 y con /32 |
| Túnel de Cloudflare caído 3 días en silencio tras una actualización (QUIC sobre una red inestable) | Forzar HTTP/2; hacen falta alertas y comprobación externa |
| Argo CD meses sin sincronizar por una credencial caducada | Una credencial con caducidad sin alerta es una avería programada |
| Volumen de Grafana en bucle de reconstrucción por WiFi | Ningún volumen Longhorn con localidad en un nodo por WiFi |
| Falco sin sonda eBPF tras un kernel nuevo | Comprobar Falco en los nodos reiniciados antes de seguir con el resto |
kubectl drain colgado por tickets huérfanos de Longhorn |
Buscar tickets snapshot-controller-* huérfanos antes de un drain |
| Pérdida de datos en local-path al migrar a HA y por un init-container destructivo | Revisar initContainers antes de reiniciar un pod con mucho tiempo arrancado |
Automatización
- Portal de mantenimiento (ansible/): Ansible detrás de Semaphore con botones para revisar, actualizar y reiniciar nodos uno a uno (cordon → drain → reboot → uncordon), comprobando etcd, Longhorn y la VIP antes de pasar al siguiente. Usuario de automatización con una sola clave restringida por origen.
- Revisión automática de PRs: cada cambio pasa un escáner de secretos y la validación de YAML y manifiestos de Kubernetes, y después una revisión con IA según las reglas del repo. Nunca aprueba ni mergea: eso lo hace una persona.
- Scripts de operación (scripts/): semáforo de seguridad de todo el clúster en ~20 s (
seguridad.py), revisión de salud, exportación del estado sin secretos, paso de un master de WiFi a cable sin perder la VIP, SSH solo con clave en los 6 nodos aplicado por Kubernetes sin entrar por SSH.
Estructura
.
├── docs/ Arquitectura, almacenamiento, operación, copias e incidentes
├── nodes/ Configuración fuera de Kubernetes: VIP (dummy0, keepalived, haproxy)
├── ansible/ Portal de mantenimiento: playbooks, Semaphore e inventario
├── scripts/ Operación, seguridad y exportación
├── seguridad/ Trivy Operator y panel de Grafana
└── monitoring/ Grafana con volumen persistente
Licencia
MIT.