HomelabKubernetes

He reemplazado mi VPS por un clúster de Raspberry Pi con K3s

Raspberry Pi K3s cluster

Durante bastante tiempo, mi infraestructura doméstica dependía de un VPS.

Era una solución sencilla: alquilar una máquina, instalar Docker y desplegar ahí los servicios que necesitaba. Para muchas cosas, un VPS es prácticamente perfecto. No tienes que preocuparte por el hardware, la electricidad, la conexión a Internet o por si un disco decide morir un domingo por la tarde.

Pero con el tiempo mi homelab empezó a crecer.

Cada vez tenía más servicios, más almacenamiento y más ganas de experimentar. Y llegó un momento en el que mantener un VPS con suficientes recursos para ejecutar todo dejó de tener demasiado sentido para mí.

Así que decidí hacer algo bastante menos razonable:

reemplazar el VPS por un clúster de Raspberry Pi.

No porque sea mejor.

No porque sea más eficiente.

Sino porque quería tener mi propia infraestructura, poder experimentar con ella y aprovechar hardware que ya tenía.

El resultado es un pequeño clúster de K3s que actualmente está formado por cinco nodos activos, con un sexto preparado para incorporarse cuando consiga solucionar algunos problemas de comunicación.

Y, aunque pueda parecer una infraestructura bastante más complicada que simplemente pagar un VPS, tiene algunas ventajas que me gustan muchísimo.


¿Por qué abandoné el VPS?

La razón principal fue bastante sencilla:

el precio.

Mi VPS empezaba a resultar demasiado caro para el uso que realmente le estaba dando. Al principio tenía sentido centralizar ahí todos mis servicios, pero conforme mi infraestructura crecía necesitaba cada vez más recursos.

Y si quería seguir creciendo, tenía dos opciones: pagar más por el VPS o buscar otra forma de conseguir capacidad.

Preferí la segunda.

Además, quería tener más control sobre la infraestructura. Si voy a estar manteniendo servidores, configurando redes, almacenamiento y Kubernetes de todas formas, ¿por qué no hacerlo con hardware que ya tengo?

Sin embargo, había un problema.

No quería abrir puertos directamente en mi red doméstica.

No me apetecía tener que exponer mi conexión a Internet y modificar el router cada vez que quisiera publicar un servicio nuevo. Así que decidí mantener un VPS, pero con una función completamente diferente.

Busqué un VPS hiperbarato, de aproximadamente 1 € al mes.

Y ese VPS ya no es mi servidor principal.

Es simplemente mi puerta de entrada a Internet.


El VPS como puerta de entrada

La idea es bastante sencilla:

                       INTERNET


                    ┌─────────────┐
                    │    Caddy    │
                    │ HTTPS / TLS │
                    └──────┬──────┘


                    ┌─────────────┐
                    │    FRPS     │
                    │     VPS     │
                    └──────┬──────┘

                       túnel FRP


                    ┌─────────────┐
                    │    FRPC     │
                    │   Mi red    │
                    └──────┬──────┘


                    ┌─────────────┐
                    │   Traefik   │
                    │    K3s      │
                    └──────┬──────┘

             ┌─────────────┼─────────────┐
             ▼             ▼             ▼
          Immich       Nextcloud      Otros servicios

Para esto utilizo FRP (Fast Reverse Proxy).

FRP está diseñado precisamente para exponer servicios que están detrás de un NAT o firewall sin necesidad de hacerlos directamente accesibles desde Internet. Utiliza un frps en el servidor público y uno o varios frpc que establecen conexiones desde la red privada.

En mi caso, el VPS ejecuta frps y mi infraestructura ejecuta frpc.

Por encima de FRP tengo Caddy, que se ocupa de la entrada HTTPS y del certificado wildcard. Caddy dispone de gestión automática de HTTPS y permite trabajar con certificados wildcard cuando se configura para ello.

Una vez que el tráfico atraviesa FRP y llega a casa, entra en Traefik, que se encarga del routing dentro de Kubernetes.

Me gusta especialmente esta separación porque cada componente tiene una responsabilidad bastante clara:

Así, si mañana despliego un servicio nuevo, no necesito abrir otro puerto en el router.

Creo el servicio y su configuración de Ingress en Kubernetes y dejo que Traefik se encargue del resto.

Tengo el control de toda la cadena.


El clúster: seis Raspberry Pi

La infraestructura de cómputo está formada por seis Raspberry Pi.

Bueno, técnicamente.

Actualmente tengo cinco funcionando dentro de K3s y una sexta esperando a que consiga solucionar unos problemas de comunicación entre nodos.

No todas las máquinas tienen exactamente el mismo hardware, y eso es algo que para mí resulta interesante.

No estoy intentando construir un clúster formado por seis servidores idénticos. Estoy aprovechando lo que tengo.

Cada Raspberry Pi tiene uno o varios SSD conectados por USB, y las capacidades son diferentes.

Esto podría parecer un problema si cada nodo tuviese que gestionar sus propios datos, pero precisamente por eso utilizo una capa de almacenamiento distribuido.


MooseFS: convertir muchos discos en uno

Para el almacenamiento utilizo MooseFS.

La idea es que los SSD de las diferentes Raspberry Pi formen un sistema de archivos distribuido. Los datos se almacenan en chunks repartidos entre los diferentes Chunkservers y el sistema puede mantener varias copias de cada uno.

En mi caso he configurado tres réplicas por chunk.

Esto significa que si tengo un chunk determinado, no existe una única copia:

Raspberry Pi 1 ── Chunk A
Raspberry Pi 2 ── Chunk A
Raspberry Pi 3 ── Chunk A

Si uno de los discos falla:

Raspberry Pi 1 ── Chunk A
Raspberry Pi 2 ── ❌
Raspberry Pi 3 ── Chunk A

MooseFS puede detectar que el número de copias ha caído por debajo del nivel configurado y volver a replicar el chunk en otro Chunkserver disponible. La propia documentación describe el goal como el nivel deseado de redundancia y explica que MooseFS gestiona dinámicamente la replicación para mantenerlo.

Por tanto:

fallo de disco

faltan réplicas

MooseFS detecta el problema

se crea otra copia

vuelvo a tener 3 réplicas

Esto también significa que si una Raspberry Pi completa deja de funcionar, las otras copias de sus chunks pueden seguir disponibles en otros nodos.

Y para mí esa es una característica fundamental.

No quiero que la disponibilidad de un servicio dependa de una Raspberry Pi concreta.


Los metadatos también tienen copias

Los datos no son la única información importante.

MooseFS tiene un Master Server que mantiene la estructura del sistema de archivos y la información necesaria para saber dónde están los chunks. Los Chunkservers son los que almacenan los datos propiamente dichos.

Por eso también tengo tres copias de los metadatos mediante Metaloggers.

Los Metaloggers reciben y almacenan información del Master y proporcionan una vía de recuperación si el Master principal falla. La documentación de MooseFS recomienda además mantener copias adicionales de los metadatos.

Pero aquí aparece una de las limitaciones de mi configuración.

Estoy utilizando la versión open-source/Community de MooseFS, no MooseFS Pro.

En Pro existe el concepto de Follower Master, que puede tomar el control si falla el Leader Master. En Community, los Metaloggers sirven como copia de seguridad de los metadatos, pero no tengo ese failover automático del Master.

Así que tengo una situación curiosa:

los datos están muy redundados, pero el Master sigue siendo un punto único de fallo.

Si pierdo un disco, MooseFS puede recuperar sus chunks.

Si pierdo un Chunkserver, las otras réplicas continúan disponibles.

Pero si pierdo el Master, toca intervenir manualmente para recuperar el sistema utilizando los metadatos disponibles.

Para un entorno empresarial probablemente no sería una decisión aceptable.

Para mi homelab, me parece un compromiso razonable.


MooseFS y Kubernetes

La siguiente pregunta era evidente:

¿cómo hago que K3s pueda utilizar ese almacenamiento?

Aquí entra el CSI de MooseFS.

MooseFS proporciona un driver CSI para Kubernetes que permite utilizar sus volúmenes directamente desde los workloads de Kubernetes. El driver permite definir StorageClass, crear PersistentVolumeClaim y realizar el aprovisionamiento dinámico de almacenamiento. También permite utilizar almacenamiento con ReadWriteMany, algo especialmente interesante en un sistema distribuido.

De esta manera, una aplicación no necesita saber dónde está físicamente su almacenamiento.

Por ejemplo:

                  Kubernetes


             PersistentVolumeClaim


                MooseFS CSI


                   MooseFS

       ┌──────────────┼──────────────┐
       ▼              ▼              ▼
     SSD 1          SSD 2          SSD 3
       │              │              │
     Pi 1            Pi 2            Pi 3

Si Kubernetes mueve un Pod de un nodo a otro, el almacenamiento persistente sigue estando disponible a través de MooseFS.

Esto separa dos cosas que me interesa muchísimo mantener separadas:

dónde se ejecuta una aplicación y dónde están sus datos.

Y esa separación es especialmente importante en un clúster donde los nodos pueden desaparecer y reaparecer.


¿Es eficiente?

No.

Y creo que es importante decirlo claramente.

No he montado esto porque sea la solución más eficiente.

Tengo Raspberry Pi, SSDs USB, una red doméstica, varias capas de software y tres copias de cada chunk.

Los datos tienen que viajar por la red.

La redundancia consume espacio.

MooseFS añade complejidad.

Kubernetes añade todavía más.

Si quisiera únicamente obtener el máximo rendimiento posible por euro, probablemente habría soluciones bastante mejores.

Pero hay algo que esas soluciones no me dan:

esto es mío.

No estoy pagando almacenamiento cloud por cada gigabyte.

No estoy limitado a los recursos de un VPS.

No necesito comprar seis servidores idénticos.

Puedo reutilizar hardware que ya tengo, añadir discos diferentes y ampliar el sistema cuando quiera.

Así que, si tuviera que resumir la filosofía del almacenamiento en una frase:

No es eficiente, pero es barato y mío.

Y para un homelab, sinceramente, me parece un intercambio bastante razonable.


¿Por qué K3s?

Llegados a este punto faltaba elegir cómo gestionar todos estos nodos.

Ya había utilizado Docker Swarm anteriormente.

Al principio me gustaba bastante: era sencillo de entender, fácil de desplegar y no necesitaba la complejidad de Kubernetes.

Pero acabé encontrándome con un problema importante:

el mantenimiento de los nodos.

En un homelab es bastante habitual apagar una Raspberry Pi.

Puede ser para cambiar un SSD, actualizar Debian, cambiar la refrigeración, mover el equipo o simplemente trastear con él.

Con Docker Swarm, mi experiencia fue que cuando uno de los nodos desaparecía por mantenimiento, los restantes podían terminar soportando demasiada carga.

Y cuando estás utilizando Raspberry Pi, no hace falta demasiado para llevarlas al límite.

Por eso decidí pasarme a K3s.

K3s es una distribución ligera y plenamente compatible con Kubernetes, pensada precisamente para escenarios como edge computing, homelabs y dispositivos ARM. Su arquitectura separa los nodos server de los agent y reduce considerablemente la cantidad de componentes necesarios para ejecutar Kubernetes.

Pero lo realmente interesante para mí no es simplemente utilizar Kubernetes.

Es todo lo que puedo hacer encima.


Descheduler: cuando un nodo vuelve

Cuando apago un nodo para mantenimiento, obviamente sus workloads tienen que ejecutarse en otros nodos.

El problema aparece cuando el nodo vuelve.

No quiero que ocurra esto:

ANTES

Pi 1 ████████
Pi 2 ███████
Pi 3 ████████
Pi 4 ███████
Pi 5 ███████

Pi 3 se apaga

DESPUÉS

Pi 1 ██████████
Pi 2 ██████████
Pi 4 ██████████
Pi 5 ██████████

Pi 3 vuelve...

Pi 1 ██████████
Pi 2 ██████████
Pi 3 ██
Pi 4 ██████████
Pi 5 ██████████

Quiero que el nuevo nodo pueda volver a participar de verdad.

Para eso utilizo Descheduler.

Descheduler puede detectar situaciones en las que los Pods existentes están distribuidos de una manera que ya no es la ideal y expulsarlos para que vuelvan a pasar por el scheduler de Kubernetes. Entre sus estrategias existen precisamente mecanismos para distribuir Pods entre nodos y corregir situaciones provocadas, por ejemplo, por la caída y posterior recuperación de nodos.

Por ejemplo, LowNodeUtilization puede detectar nodos infrautilizados y desalojar Pods de nodos sobreutilizados con la intención de que sean reprogramados en los nodos que tienen recursos disponibles.

Así puedo conseguir el comportamiento que buscaba:

Nodo falla

Pods se reprograman en otros nodos

Los nodos restantes soportan temporalmente la carga

Nodo vuelve

Descheduler detecta el desequilibrio

Evacúa determinados Pods

Kubernetes los vuelve a programar

Los Pods vuelven a distribuirse

La combinación es importante: Descheduler no decide dónde colocar el Pod reemplazado; lo expulsa y deja que el scheduler de Kubernetes vuelva a tomar esa decisión.


Horizontal Pod Autoscaler

A esto le añado Horizontal Pod Autoscaler (HPA).

El HPA permite modificar automáticamente el número de réplicas de un workload en función de métricas. Cuando aumenta la carga, puede aumentar el número de Pods; cuando disminuye, puede reducirlos.

Por tanto, tengo tres piezas trabajando juntas:

              demanda


                HPA

        ¿Cuántos Pods necesito?


          Kubernetes Scheduler

          ¿Dónde los coloco?


              Pods


         cambios en el clúster


            Descheduler

       ¿Están bien distribuidos?


         Evict → Scheduler

Esto me permite aprovechar mucho mejor los recursos de los nodos.

No es un sistema mágico, y obviamente no puedo hacer que cinco Raspberry Pi mantengan mágicamente la misma carga cuando una sexta desaparece.

Pero sí puedo conseguir que el sistema reaccione a los cambios de capacidad.


Y puedo añadir más nodos

Aquí aparece otra de las ventajas que más me gustan de esta arquitectura.

No necesito que todos los nodos sean iguales.

Utilizo Ansible para automatizar la configuración de las máquinas.

Ansible trabaja con un inventario de hosts y permite agrupar máquinas y definir variables para ellas, lo que resulta muy útil cuando tienes hardware heterogéneo.

Por tanto, si dentro de unos meses necesito más potencia, puedo añadir otra máquina.

Puede ser:

No necesito sustituir los nodos existentes.

Simplemente añado capacidad.

El proceso que busco es básicamente:

Tengo hardware nuevo

Lo añado al inventario de Ansible

Ansible prepara la máquina

Se incorpora a K3s

Kubernetes obtiene más recursos

HPA puede crear más réplicas

Scheduler las distribuye

Descheduler ayuda a mantener el equilibrio

Esto hace que mi infraestructura pueda crecer de una manera bastante orgánica.

Una Raspberry Pi antigua no tiene por qué acabar en un cajón porque ya no sea suficientemente potente para ser mi servidor principal.

Si puede aportar CPU, RAM o almacenamiento, puede convertirse en otro nodo.

Y si mañana consigo un mini PC con diez veces más potencia, también puede formar parte del mismo clúster.


¿Y la red interna?

Aquí todavía tengo una decisión pendiente.

Actualmente FRP es mi mecanismo de entrada, pero todavía no tengo decidida la solución definitiva para la red privada entre todos los componentes.

Probé Tailscale, pero en mi entorno las conexiones terminaban recurriendo demasiado a menudo a sus relays DERP cuando no conseguían establecer una conexión directa.

Esto tiene una explicación bastante sencilla.

Tailscale utiliza conexiones directas UDP siempre que puede, pero cuando el NAT o el firewall impiden establecerlas, puede utilizar un relay DERP. Las conexiones directas normalmente tienen menor latencia y mayor rendimiento, mientras que las conexiones relayed son una alternativa de fallback.

En mi caso, ese comportamiento era demasiado lento para lo que quería hacer con la red.

La otra opción que me interesa es NetBird.

El problema es que el despliegue self-hosted necesita infraestructura pública. Actualmente su guía de despliegue requiere, como mínimo, una VM Linux con 1 CPU y 2 GB de RAM, además de conectividad pública y determinados puertos.

Y precisamente estoy intentando reducir al máximo la cantidad de infraestructura externa que tengo que pagar.

Así que, por ahora, lo dejo pendiente.

Mi idea para el futuro es utilizar un VPS muy sencillo exclusivamente para NetBird.

No necesito que ese VPS ejecute aplicaciones.

Solo quiero que proporcione la infraestructura necesaria para mi red privada.


La arquitectura actual

Después de todo esto, mi infraestructura termina pareciéndose bastante a un pequeño datacenter doméstico:

                              INTERNET


                         ┌─────────────────┐
                         │ VPS económico   │
                         │                 │
                         │ Caddy           │
                         │       ↓         │
                         │      FRPS       │
                         └────────┬────────┘

                              túnel FRP


                         ┌─────────────────┐
                         │      FRPC       │
                         │   Red privada   │
                         └────────┬────────┘


                    ┌──────────────────────────┐
                    │          K3s             │
                    │                          │
                    │         Traefik          │
                    │            │             │
                    │      ┌─────┴─────┐       │
                    │      │ Kubernetes│       │
                    │      └─────┬─────┘       │
                    │            │              │
                    │   HPA + Descheduler       │
                    │            │              │
                    └────────────┼──────────────┘

               ┌─────────────────┼─────────────────┐
               │                 │                 │
               ▼                 ▼                 ▼
            Raspberry Pi      Raspberry Pi      Raspberry Pi
               │                 │                 │
              SSD               SSD               SSD
               │                 │                 │
               └─────────────────┼─────────────────┘

                              MooseFS

                       3 réplicas por chunk

                         3 copias de metadata

Y en medio de todo esto tengo Ansible para poder convertir una máquina nueva en otro miembro de la infraestructura sin tener que repetir manualmente toda la configuración.


¿Ha merecido la pena?

Si la pregunta es si esto es mejor que un VPS, la respuesta es:

depende de lo que entiendas por mejor.

Si quiero una máquina potente, sencilla y que simplemente funcione, probablemente debería seguir pagando un VPS.

Mi solución tiene muchas más piezas.

Tengo Raspberry Pi que pueden fallar.

Tengo SSDs USB.

Tengo almacenamiento distribuido.

Tengo Kubernetes.

Tengo FRP.

Tengo Caddy.

Tengo que preocuparme de la red.

Tengo que solucionar problemas de nodos.

Y, ocasionalmente, tengo que pasar una tarde entera intentando descubrir por qué una Raspberry Pi se niega a hablar correctamente con K3s.

Pero también tengo algo que un VPS no me daba:

control.

Puedo añadir hardware.

Puedo quitarlo.

Puedo mezclar arquitecturas.

Puedo ampliar almacenamiento con otro SSD.

Puedo apagar un nodo para hacer mantenimiento.

Puedo experimentar con Kubernetes.

Puedo cambiar la red.

Puedo romper cosas y volver a arreglarlas.

Y, sobre todo, puedo aprovechar hardware que ya tenía.

La infraestructura tampoco pretende ser eficiente.

De hecho, es bastante probable que si alguien con experiencia en datacenters mira este montaje encuentre unas cuantas cosas que le hagan levantar una ceja.

Pero esa nunca fue la finalidad.

No estoy construyendo AWS.

No estoy intentando montar un datacenter profesional.

Estoy construyendo mi propio pequeño datacenter en casa.

Y si tuviera que resumir todo el proyecto en una frase, sería probablemente esta:

No es eficiente, pero es barato y mío.

Y para un homelab, eso me parece un motivo bastante bueno para hacerlo.