Una StorageClass le dice a Kubernetes cómo crear volúmenes persistentes bajo demanda: qué driver usar, con qué parámetros y qué hacer con los datos cuando se borra el volumen. Los drivers CSI (Container Storage Interface) son los plugins que hablan con el almacenamiento real, ya sea NFS, Ceph o el disco de un proveedor cloud. En este tutorial montarás un servidor NFS en Ubuntu 24.04, instalarás el driver oficial csi-driver-nfs en tu clúster y crearás una StorageClass que aprovisiona volúmenes ReadWriteMany automáticamente.
Requisitos previos
Para seguir esta guía necesitas:
- Un clúster de Kubernetes 1.30 o superior (kubeadm, k3s o similar) sobre servidores Ubuntu 24.04, por ejemplo VPS de CubePath conectados por red privada.
kubectlconfigurado con permisos de administrador del clúster y Helm 3 instalado en tu equipo.- Un servidor adicional con Ubuntu 24.04 y un usuario no root con
sudo, que hará de servidor NFS. Puede ser un nodo del clúster en un entorno de pruebas. - Conectividad entre los nodos y el servidor NFS por el puerto TCP 2049, idealmente por red privada.
Conceptos: qué define una StorageClass
Antes de instalar nada conviene saber qué campos vas a configurar:
| Campo | Qué hace | Valores habituales |
|---|---|---|
provisioner | Driver CSI que crea los volúmenes | nfs.csi.k8s.io, rbd.csi.ceph.com, rancher.io/local-path |
parameters | Opciones propias del driver (servidor, pool, tipo de disco) | Dependen del driver |
reclaimPolicy | Qué pasa con el volumen al borrar el PVC | Delete (por defecto) o Retain |
volumeBindingMode | Cuándo se crea el volumen | Immediate o WaitForFirstConsumer |
allowVolumeExpansion | Permite ampliar un PVC editando su tamaño | true o false |
mountOptions | Opciones de montaje que se aplican en el nodo | nfsvers=4.1, noatime |
El flujo es siempre el mismo: una aplicación crea un PersistentVolumeClaim (PVC) que pide una StorageClass, el driver CSI crea el volumen en el backend y Kubernetes genera el PersistentVolume (PV) y lo enlaza al PVC.
Comprueba qué clases tiene ya tu clúster:
kubectl get storageclass
En un clúster k3s verás la clase local-path, marcada como predeterminada. En un clúster kubeadm recién creado la lista suele estar vacía:
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
local-path (default) rancher.io/local-path Delete WaitForFirstConsumer false 12d
local-path guarda los datos en el disco de un solo nodo, por lo que el volumen no sigue al pod si este cambia de nodo. NFS resuelve eso y además permite que varios pods escriban a la vez.
Paso 1: Preparar el servidor NFS
En el servidor NFS, instala el paquete del servidor:
sudo apt update
sudo apt install nfs-kernel-server
Crea el directorio que exportarás. El driver creará dentro un subdirectorio por cada volumen:
sudo mkdir -p /srv/nfs/k8s
Declara la exportación en /etc/exports. Sustituye 10.0.0.0/24 por el rango de tu red privada, para que solo los nodos del clúster puedan montarla:
sudo nano /etc/exports
/srv/nfs/k8s 10.0.0.0/24(rw,sync,no_subtree_check,no_root_squash)
La opción no_root_squash es necesaria para que el driver, que se ejecuta como root, pueda crear los subdirectorios y ajustar sus permisos. Por eso es importante limitar la exportación a la red privada.
Aplica la configuración y comprueba que la exportación está activa:
sudo exportfs -ra
sudo exportfs -v
/srv/nfs/k8s 10.0.0.0/24(sync,wdelay,hide,no_subtree_check,sec=sys,rw,secure,no_root_squash,no_all_squash)
Si usas UFW, permite NFS solo desde la red privada:
sudo ufw allow from 10.0.0.0/24 to any port 2049 proto tcp
Paso 2: Instalar el cliente NFS en los nodos
Cada nodo que vaya a ejecutar pods con volúmenes NFS necesita las utilidades de montaje. Ejecuta en todos los nodos del clúster:
sudo apt update
sudo apt install nfs-common
Desde uno de los nodos, comprueba que ve la exportación. Sustituye 10.0.0.10 por la IP privada de tu servidor NFS:
showmount -e 10.0.0.10
Export list for 10.0.0.10:
/srv/nfs/k8s 10.0.0.0/24
Paso 3: Instalar el driver CSI de NFS con Helm
El proyecto kubernetes-csi/csi-driver-nfs mantiene el driver oficial y publica su chart de Helm en GitHub. Añade el repositorio:
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm repo update
Instala el driver en el namespace kube-system:
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs --namespace kube-system
Consejoen producción fija la versión con
--versionpara que una reinstalación no cambie de versión sin que lo decidas.helm search repo csi-driver-nfsmuestra las disponibles.
El chart despliega un controlador (csi-nfs-controller), que crea y borra los volúmenes, y un DaemonSet (csi-nfs-node), que los monta en cada nodo. Espera a que todos los pods estén en Running:
kubectl -n kube-system get pods -l app.kubernetes.io/instance=csi-driver-nfs
NAME READY STATUS RESTARTS AGE
csi-nfs-controller-6f8b9c7d5b-x2lqp 4/4 Running 0 45s
csi-nfs-node-7kq2m 3/3 Running 0 45s
csi-nfs-node-p9z4d 3/3 Running 0 45s
El driver queda registrado en el clúster como un objeto CSIDriver:
kubectl get csidrivers
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
nfs.csi.k8s.io false false false <unset> false Persistent 1m
Paso 4: Crear la StorageClass
Crea un archivo con la definición de la clase:
nano nfs-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: 10.0.0.10
share: /srv/nfs/k8s
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
server y share indican al driver dónde crear los volúmenes. Con reclaimPolicy: Delete, el subdirectorio de cada volumen se borra al eliminar su PVC. Para datos que no quieras perder por un borrado accidental, más adelante verás cómo usar Retain.
Aplica el manifiesto y comprueba la clase:
kubectl apply -f nfs-storageclass.yaml
kubectl get storageclass nfs-csi
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
nfs-csi nfs.csi.k8s.io Delete Immediate false 5s
Si quieres que los PVC que no indican storageClassName usen esta clase, márcala como predeterminada. Antes quita la marca de la clase que la tenga (en k3s, local-path), porque solo debe haber una:
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
kubectl patch storageclass nfs-csi -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
Paso 5: Aprovisionar un volumen de forma dinámica
Crea un PVC que pida 5 GiB en modo ReadWriteMany, que permite montarlo desde varios pods y nodos a la vez:
nano shared-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: shared-data
spec:
accessModes:
- ReadWriteMany
storageClassName: nfs-csi
resources:
requests:
storage: 5Gi
kubectl apply -f shared-pvc.yaml
kubectl get pvc shared-data
En pocos segundos el PVC pasa a Bound y aparece un PV creado automáticamente:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
shared-data Bound pvc-3f1c2a9e-8d4b-4c61-9a7e-2b5d0e6f1a23 5Gi RWX nfs-csi 4s
En el servidor NFS verás el subdirectorio que ha creado el driver:
ls /srv/nfs/k8s
pvc-3f1c2a9e-8d4b-4c61-9a7e-2b5d0e6f1a23
NotaNFS no aplica cuotas, así que los 5 GiB del PVC son orientativos. El límite real es el espacio libre del disco exportado.
Paso 6: Usar el volumen desde varios pods
Para comprobar que el volumen es compartido, crea un Deployment con dos réplicas en el que cada pod escribe su nombre en el mismo archivo:
nano writers.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: writers
spec:
replicas: 2
selector:
matchLabels:
app: writers
template:
metadata:
labels:
app: writers
spec:
containers:
- name: writer
image: busybox:1.36
command: ["sh", "-c", "while true; do echo \"$(date) $HOSTNAME\" >> /data/log.txt; sleep 10; done"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: shared-data
kubectl apply -f writers.yaml
kubectl rollout status deployment/writers
Pasado medio minuto, lee el archivo desde uno de los pods. Deberías ver líneas de los dos:
kubectl exec deploy/writers -- tail -n 4 /data/log.txt
Thu Sep 24 10:12:01 UTC 2026 writers-7c9d8f6b4-2xkqz
Thu Sep 24 10:12:03 UTC 2026 writers-7c9d8f6b4-mw7tn
Thu Sep 24 10:12:11 UTC 2026 writers-7c9d8f6b4-2xkqz
Thu Sep 24 10:12:13 UTC 2026 writers-7c9d8f6b4-mw7tn
Paso 7: Conservar datos con la política Retain
Con Delete, borrar el PVC borra también los datos. Para bases de datos u otros datos importantes es más seguro Retain: el PV y su contenido se conservan y tú decides qué hacer con ellos.
Los parámetros de una StorageClass no se pueden modificar una vez creada, así que crea una segunda clase:
nano nfs-retain.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi-retain
provisioner: nfs.csi.k8s.io
parameters:
server: 10.0.0.10
share: /srv/nfs/k8s
reclaimPolicy: Retain
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
kubectl apply -f nfs-retain.yaml
También puedes cambiar la política de un PV que ya existe, por ejemplo el que acabas de crear. Sustituye el nombre por el de tu PV:
kubectl patch pv pvc-3f1c2a9e-8d4b-4c61-9a7e-2b5d0e6f1a23 -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
Borra ahora el Deployment y el PVC, y observa el PV:
kubectl delete deployment writers
kubectl delete pvc shared-data
kubectl get pv
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
pvc-3f1c2a9e-8d4b-4c61-9a7e-2b5d0e6f1a23 5Gi RWX Retain Released default/shared-data nfs-csi 9m
El PV queda en estado Released y el archivo log.txt sigue en el servidor NFS. Un PV liberado no se reutiliza automáticamente: cuando ya no necesites los datos, borra el PV con kubectl delete pv <nombre> y elimina el subdirectorio a mano en el servidor NFS.
Notala política
Recycleestá obsoleta y ningún driver CSI la soporta. UsaDeleteoRetain.
Otros drivers CSI habituales
El procedimiento es el mismo con cualquier driver: instalarlo (normalmente con Helm), crear una StorageClass con su provisioner y sus parameters, y pedir volúmenes con PVC.
| Driver | Provisioner | Cuándo usarlo |
|---|---|---|
| csi-driver-nfs | nfs.csi.k8s.io | Volúmenes compartidos ReadWriteMany con un servidor NFS existente |
| Ceph RBD (mediante Rook) | <namespace>.rbd.csi.ceph.com | Bloques replicados de alto rendimiento en varios nodos |
| local-path-provisioner | rancher.io/local-path | Pruebas o clústeres de un nodo, datos ligados a un nodo |
Para almacenamiento de bloque (bases de datos), usa volumeBindingMode: WaitForFirstConsumer: así el volumen se crea en la zona o nodo donde el scheduler coloca el pod, y no al revés.
Solución de problemas
El PVC se queda en Pending. Revisa los eventos del PVC y los registros del controlador:
kubectl describe pvc shared-data
kubectl -n kube-system logs deploy/csi-nfs-controller -c nfs
Un error access denied by server indica que la IP del nodo no está en el rango de /etc/exports. Un Connection timed out apunta al firewall del puerto 2049.
El pod se queda en ContainerCreating con bad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helper program. Falta nfs-common en el nodo donde se programó el pod. Instálalo como en el paso 2.
kubectl apply falla al cambiar una StorageClass con Forbidden: updates to parameters are forbidden. Los campos provisioner, parameters y reclaimPolicy son inmutables. Borra la clase con kubectl delete storageclass <nombre> y vuelve a crearla; los PV existentes no se ven afectados.
Conclusión
Ya tienes un servidor NFS exportando a tu clúster, el driver csi-driver-nfs instalado y dos StorageClass que crean volúmenes compartidos bajo demanda, una que borra los datos con el PVC y otra que los conserva. Como siguientes pasos puedes usar la clase en el volumeClaimTemplates de un StatefulSet, programar copias de seguridad del directorio exportado o evaluar Rook con Ceph si necesitas almacenamiento replicado sin un único servidor NFS como punto de fallo.
