Desplegando un servidor NFS en Kubernetes
from Sugui's tech trip
Hacía tiempo que quería tener algún tipo de almacenamiento compartido, algo que quería sobre todo para guardar mi música localmente, sin usar servicios externos, y poder reproducirla en múltiples dispositivos.
El dilema de la sincronización
La primera opción que se me vino a la cabeza es un software que ya uso actualmente para sincronizar ficheros, llamado Syncthing. Esta es una herramienta para sincronizar ficheros entre dispositivos de manera peer-to-peer. El problema de usarla es que los ficheros ocupan espacio en cada uno de los dispositivos, y el tamaño total de los ficheros que quiero guardar sube a unos cuantos GiB, por lo que descarté esa opción.
Dado que tengo un homelab en casa, pensé en una manera mejor: montarme un servidor NFS. ¿Y qué es NFS? Pues es un protocolo sencillo para compartir sistemas de ficheros sobre una red. La topología de NFS consiste en un servidor y varios clientes, que acceden a través de la red al sistema de ficheros que se encuentra en el servidor. Esto me elimina la desventaja que tenía con Syncthing, ya que los ficheros estarían en el vasto almacenamiento del homelab, por lo que evitamos gastar espacio innecesario en cada dispositivo.
Sobre el homelab
Este homelab que he comentado consiste en un clúster de Kubernetes con varias máquinas o nodos. Uno de ellos, llamado bastiodon
, está dedicado a servir aplicaciones stateful, es decir, que usan almacenamiento persistente. Parece bastante claro que un servidor NFS debe de usar almacenamiento persistente, ¡no queremos que se reinicie la máquina y se hayan perdido nuestros datos! Así que decidí montarlo en ese nodo.
Si tienes curiosidad de por qué se llama bastiodon
, sí, es por Pokémon. Bastiodon es una bestia grande y pesada, igual que el nodo :)
Pruebas en local
El primer paso fue buscar una imagen Docker para poder usar, y encontré la siguiente: https://github.com/ehough/docker-nfs-server. Antes de probar a meterla en el clúster, hice pruebas con ella en local.
He de decir que, a pesar de que estaba bastante bien documentada, hubo detalles que tuve que averiguar por mí misma. Este es el comando que ejecuté para desplegar localmente el servidor NFS:
docker run \
-v /home/sugui/nfs:/shared \
-v /lib/modules:/lib/modules:ro
-e NFS_EXPORT_0='/shared *(rw,insecure,no_subtree_check,fsid=0)' \
-e NFS_DISABLE_VERSION_3=true \
--cap-add SYS_ADMIN \
--cap-add SYS_MODULE \
-p 2049:2049 \
erichough/nfs-server
Pasaré a detallar algunas partes de este comando:
– -v /home/sugui/nfs:/shared
: aquí monto el directorio de mi host como un volumen para el contenedor, será el que se sirva por NFS.
– -e NFS_EXPORT_0='/shared *(rw,insecure,no_subtree_check,fsid=0)'
: esta variable de entorno la interpreta el propio contenedor para generar el fichero /etc/exports
, usado para definir los directorios a exportar por NFS. Se pueden especificar más líneas definiendo NFS_EXPORT_1
, NFS_EXPORT_2
, ... O incluso pasándole el propio fichero con otro volumen.
– -e NFS_DISABLE_VERSION_3=true
: esta es una de las cosas que tuve que indagar por mi cuenta... Al tener habilitada la versión 3 de NFS, el contenedor que creaba localmente se quedaba sin memoria por alguna razón. Y como solo quiero usar la versión 4, desactivé la otra versión. ¡Los problemas de memoria desaparecieron!
– --cap-add SYS_ADMIN
: esto es un privilegio dado al contenedor sobre el host, necesario ya que NFS necesita montar sistemas de ficheros internamente dentro del contenedor.
– -v /lib/modules:/lib/modules:ro
y --cap-add SYS_MODULE
: el contenedor necesita que el kernel del host tenga habilitados los módulos nfs
y nfsd
. Para dar acceso al contenedor a los módulos del host, compartimos el directorio /lib/modules
como read-only. Además, para que el contenedor habilite estos módulos automáticamente en el kernel del host, le damos el permiso SYS_MODULE
.
Después de mucha prueba y error, conseguí que funcionase el directorio perfectamente con el comando mount
. Así que empecé a pasarlo al clúster.
Despliegue en el clúster Kubernetes
Dado que el servidor NFS es una aplicación stateful, no usé un Deployment
, como se suele hacer con las aplicaciones serverless. En su lugar, usé StatefulSet
, que es similar, pero tiene la característica de guardar las asociaciones entre los Pods
y los volumenes, de forma que si un pod se elimina y se vuelve a crear, siempre se desplegará junto a su volumen.
En mi homelab uso k3s, que viene con algunas cosas predefinidas. En concreto, viene con un StorageClass
por defecto llamado local-path
, que crea volúmenes (PersistentVolume
) usando el almacenamiento local del nodo. Debido a que uso la sección volumeClaimTemplates
para crear un volumen y no especifico el StorageClass
a usar, se usa el local-path
que viene por defecto, por lo que se crea un volumen usando el almacenamiento local del nodo.
Es importante destacar que para entornos más empresariales en general no es recomendable usar el storage del propio nodo, sino usar otras soluciones más robustas como Ceph, o el propio storage que te ofrezca la nube.
# statefulset.yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: nfs
spec:
selector:
matchLabels:
app: nfs
serviceName: nfs
template:
metadata:
labels:
app: nfs
spec:
nodeSelector:
kubernetes.io/hostname: bastiodon
containers:
- name: nfs
image: erichough/nfs-server:2.2.1
resources:
limits:
memory: "512Mi"
cpu: "500m"
ports:
- containerPort: 2049
env:
- name: NFS_EXPORT_0
value: "/shared *(rw,insecure,no_subtree_check,fsid=0)"
- name: NFS_DISABLE_VERSION_3
value: "true"
- name: NFS_PORT
value: "2049"
securityContext:
capabilities:
add: ["SYS_ADMIN", "SYS_MODULE"]
volumeMounts:
- mountPath: /shared
name: shared-claim
- mountPath: /lib/modules
name: lib-modules
readOnly: true
volumes:
- name: lib-modules
hostPath:
path: /lib/modules
type: Directory
volumeClaimTemplates:
- metadata:
name: shared-claim
namespace: nfs
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path
resources:
requests:
storage: 30Gi
También hice un Service
de tipo LoadBalancer
para poder exponer el servidor fuera del clúster. Dado que solamente estoy exponiendo este puerto en mi red interna, no pasa nada. Pero en general hay tener cuidado de no exponer un servidor NFS a internet sin más, ya que cualquiera podría tener acceso. NFS no proporciona demasiada seguridad aparte de poder limitar el rango de IPs que pueden acceder a la carpeta en el /etc/exports
.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: nfs
spec:
type: LoadBalancer
selector:
app: nfs
ports:
- port: 2049
targetPort: 2049
Persistiendo el montaje del directorio NFS en los dispositivos
Una vez desplegados estos recursos en el cluster, ya podemos usar nuestro directorio compartido por red. Si queremos que este directorio siempre esté montado cuando se inicie cualquiera de mis dispositivos, podemos añadir la siguiente línea al /etc/fstab
:
# <file system> <dir> <type> <options> <dump> <pass>
192.168.1.12:/ /home/sugui/Music/shared nfs defaults,nofail 0 0
De esta forma, cada vez que se inicie el dispositivo, se montará el volumen NFS automáticamente.
Finalizamos
Todavía sigue habiendo algunas posibles mejoras que se pueden aplicar, como limitar el rango de IPs a las de la red privada, a pesar de no estar exponiendo ningún puerto, más que nada por seguridad.
Este proyecto ha sido muy interesante, no solo por la utilidad que aporta el tener un directorio compartido entre dispositivos, sino también por aprender más sobre NFS, Kubernetes y Linux en general.