Desplegando un servidor NFS en Kubernetes

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.