Almacenamiento persistente en Kubernetes
La persistencia de datos es un pilar en cualquier plataforma de contenedores. Ya vimos los volúmenes en Docker, aunque Kubernetes abstrae el almacenamiento mediante varios objetos que colaboran entre sí para desacoplar:
- La infraestructura física o lógica (discos, NFS, Ceph, EBS, etc.)
- Las necesidades de las aplicaciones (tamaño, modo de acceso, rendimiento)
- El modo en que se aprovisiona (manual/estático vs automático/dinámico)
🎥 Vídeo completo, utilicemos los capítulos de youtube para saltar a la sección que nos interese: https://youtu.be/iX91NRo9cg0
Esto es así en Kubernetes porque las aplicaciones desplegadas en pods son efímeras y no deben depender del almacenamiento local del nodo. Imaginemos que un pod se elimina o se mueve a otro nodo: si el almacenamiento estuviera en el nodo local, los datos se perderían o quedarían inaccesibles.
Por eso en Kubernetes se suele trabajar el almacenamiento persistente mediante volúmenes de red, almacenamiento gestionado en la nube o sistemas distribuidos.
Lo primero, veamos los conceptos clave que lo hacen posible:
- PV (PersistentVolume): Representa una pieza de almacenamiento ya aprovisionada en el cluster.
- PVC (PersistentVolumeClaim): Solicitud de consumo de almacenamiento por parte de un namespace/aplicación.
- StorageClass: Define clases de almacenamiento con parámetros de aprovisionamiento dinámico (tipo de disco, reclamación, volumenMode, etc.).
- CSI (Container Storage Interface): Estándar que permite que proveedores de almacenamiento implementen drivers portables para Kubernetes.
Abordaremos:
- Diferencias entre aprovisionamiento estático y dinámico
- Parámetros importantes (
accessModes,reclaimPolicy,volumeMode,storageClassName,bindingMode) - Ejemplos prácticos YAML
- Diagrama de relaciones entre los objetos
- Control de cuotas con
ResourceQuota
Consejo: Antes de desplegar aplicaciones con estado (stateful) validemos requisitos de IOPS, latencia y disponibilidad para escoger la clase de almacenamiento correcta.
Tabla rápida de conceptos
| Objeto | ¿Quién lo crea? | ¿Cuándo se usa? | Rol principal | Estático | Dinámico |
|---|---|---|---|---|---|
| PV | Operador / Admin | Cuando se quiere aprovisionar manualmente | Representa almacenamiento físico/lógico | Sí | Referenciado por StorageClass (indirecto) |
| PVC | Desarrollador / App | Cuando la app necesita almacenamiento | Solicita y vincula almacenamiento | Sí (claim a PV existente) | Sí (dispara creación vía StorageClass) |
| StorageClass | Operador | Define parámetros de aprovisionamiento dinámico | Plantilla de volúmenes | No aplica directamente | Sí |
| CSI Driver | Proveedor | Instalado en cluster | Implementa operaciones de volumen | Habilita PV pre-creados | Habilita creación dinámica |
Provisionamiento Estático vs Dinámico
- La aplicación crea un PVC solicitando tamaño y modo de acceso.
- Kubernetes intenta vincular el PVC:
- Si especifica
storageClassName, se crea un PV dinámicamente usando el CSI driver asociado a esaStorageClass. - Si NO especifica
storageClassNamey el cluster tiene una StorageClass por defecto, se le asigna esa y el PV se crea dinámicamente. - Si NO hay StorageClass por defecto, o si ponemos
storageClassName: ""de forma explícita, Kubernetes busca un PV estático existente que coincida (tamaño,accessModes,volumeModey sin clase).
- Si especifica
- El PVC se vincula (Bound) y el Pod monta el volumen.
- Al eliminar el PVC, la acción sobre el PV depende de
persistentVolumeReclaimPolicy(Recycle, Retain, Delete — hoyRecycleestá deprecado en muchos escenarios).
Parámetros Fundamentales
accessModes
ReadWriteOnce (RWO): Un nodo puede montar lectura/escritura.ReadWriteMany (RWX): Varios nodos pueden montar lectura/escritura (NFS, CephFS...).ReadOnlyMany (ROX): Varios nodos lectura.ReadWriteOncePod (RWOP): Solo un pod concreto puede montar lectura/escritura (más restrictivo que RWO, que permite varios pods en el mismo nodo).
persistentVolumeReclaimPolicy
Retain: Conserva datos tras eliminar el PVC (requiere limpieza manual).Delete: Elimina el recurso subyacente (ej. disco cloud).Recycle(obsoleto): Intentaba limpiar y dejar listo para otra claim.
volumeMode
Filesystem(por defecto): El volumen se monta como sistema de ficheros.Block: Se presenta como bloque crudo (útil para BD especializadas).
bindingMode (StorageClass)
Immediate: Crea el PV en cuanto se crea el PVC.WaitForFirstConsumer: Pospone la creación hasta que un Pod que use el PVC esté programándose (optimiza localización / zona).
storageClassName
En el PVC determina la clase que se usará para aprovisionamiento dinámico. Si se omite y el cluster tiene una StorageClass marcada como default (lo habitual en clusters gestionados y en muchas instalaciones), se usa esa. Si no hay ninguna por defecto, se intenta un bind estático a un PV libre compatible. Para forzar el binding estático aunque exista una clase por defecto, hay que indicar storageClassName: "".
Aprovisionamiento Estático
En el aprovisionamiento estático el administrador crea manualmente uno o varios PV apuntando a recursos existentes (ej: NFS, disco montado, LUN). Luego los desarrolladores crean PVC que se enlazan a esos PV.
Ejemplo PV Estático (NFS)
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-nfs-demo
labels:
tipo: compartido
spec:
capacity:
storage: 40Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
volumeMode: Filesystem
nfs:
path: /ruta/carpeta/nfs
server: <host> # Puede ser un servidor NFS interno
readOnly: false
PVC que reclama ese PV
Podemos usar selector.matchLabels para forzar que el claim se vincule sólo a PV con etiquetas específicas.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-nfs-demo
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: "" # Forzamos binding estático aunque exista una StorageClass por defecto
selector:
matchLabels:
tipo: compartido
volumeMode: Filesystem
Si el PV ofrece más capacidad que la solicitada por el PVC (ej. 40Gi vs 10Gi), el bind aún es válido, pero el PVC se queda con el PV entero: los 30Gi sobrantes no los puede usar ningún otro PVC.
Aplicación:
kubectl apply -f pv-nfs.yaml
kubectl apply -f pvc-nfs.yaml
Comprobar estado:
kubectl get pv
kubectl get pvc
Estado esperado: STATUS: Bound en el PVC.
Aprovisionamiento Dinámico con StorageClass + CSI
El aprovisionamiento dinámico evita la creación manual de PV. Cuando un PVC especifica una storageClassName, Kubernetes delega la creación de un PV en el provisioner definido por esa StorageClass (normalmente parte de un CSI driver).
¿Qué es CSI?
Container Storage Interface es un estándar de la CNCF que permite que los proveedores implementen un driver único para diferentes orquestadores. En Kubernetes, el CSI driver corre como Pods/DaemonSets y expone capacidades de crear, montar, snapshot, expandir volúmenes, etc.
Ejemplo StorageClass (NFS CSI)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
# Parámetros específicos del driver (dependen del proveedor)
reclaimPolicy: Retain
volumeBindingMode: Immediate
allowVolumeExpansion: true
PVC dinámico
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-dinamico-nfs
spec:
storageClassName: nfs-csi
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi
volumeMode: Filesystem
Al aplicar el PVC, el CSI provisioner creará un PV acorde.
Nota: Algunos drivers soportan expansión en caliente del volumen si
allowVolumeExpansion: truey se actualizaspec.resources.requests.storagedel PVC.
Otros ejemplos de provisioner
- AWS EBS:
provisioner: ebs.csi.aws.com(normalmente RWO) - GCP PD:
provisioner: pd.csi.storage.gke.io - Ceph RBD:
provisioner: rook-ceph.rbd.csi.ceph.com - Longhorn:
provisioner: driver.longhorn.io
Consumir un PVC desde un Pod
Ejemplo mejorado (indentación y campos clave):
apiVersion: v1
kind: Pod
metadata:
name: nginx-con-volumen
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
ports:
- containerPort: 80
volumeMounts:
- name: datos
mountPath: /usr/share/nginx/html
volumes:
- name: datos
persistentVolumeClaim:
claimName: pvc-nfs-demo # O pvc-dinamico-nfs según flujo
Aplicar:
kubectl apply -f pod-nginx.yaml
kubectl describe pod nginx-con-volumen | grep -A5 Volumes
¿Cuál usar?
| Escenario | Recomendación |
|---|---|
| Test rápido / laboratorio | PV estático sencillo (NFS) |
| Multi-tenant con distintos patrones | StorageClass con distintos parámetros (gold, silver, backup) |
| Cloud con discos gestionados | Aprovisionamiento dinámico (EBS / PD / etc.) |
| Rendimiento crítico (BD) | StorageClass específica (IOPS alto, volumenMode Block) |
| Migraciones heredadas | PV estático apuntando a almacenamiento ya existente |
Laboratorio: local-path, PV manual y Longhorn
Lo que sigue es lo que montamos en el curso presencial, sobre un clúster kubeadm con dos workers (el del capítulo 3). Tres formas de dar disco a un pod, de la más sencilla a la más completa:
| local-path | PV manual | Longhorn | |
|---|---|---|---|
| Quién crea el volumen | Un provisionador, al pedirlo | Nosotros, a mano | Un provisionador, al pedirlo |
| Dónde viven los datos | En el disco de un nodo | En el disco de un nodo | Replicados en varios nodos |
| Si cae ese nodo | El pod no puede moverse | El pod no puede moverse | El pod arranca en otro nodo con sus datos |
local-path: dinámico y sin complicaciones
Un clúster kubeadm recién montado no trae ninguna StorageClass, así que todos los PVC se quedan en Pending. El local-path-provisioner de Rancher lo arregla en un comando:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.37/deploy/local-path-storage.yaml
kubectl patch storageclass local-path -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION
local-path rancher.io/local-path Delete WaitForFirstConsumer false
Por su WaitForFirstConsumer, un PVC nuevo se queda en Pending hasta que un pod lo usa: espera a saber en qué nodo cae el pod para crear allí la carpeta (/opt/local-path-provisioner/...). No es un error. Y por su Delete, al borrar el PVC se borran también el PV y los datos.
PV manual con Retain: lo que hace el provisionador por dentro
Ahora creamos el volumen nosotros: una carpeta del nodo (hostPath) con nodeAffinity, para que Kubernetes sepa dónde están los datos y lleve allí al pod.
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-manual
spec:
capacity:
storage: 1Gi
accessModes: ["ReadWriteOnce"]
persistentVolumeReclaimPolicy: Retain # al soltarlo, NO se borran los datos
storageClassName: manual # una clase sin provisionador
hostPath:
path: /mnt/pv-manual
type: DirectoryOrCreate
nodeAffinity: # los datos están en este nodo
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: ["<worker>"]
Un PVC con storageClassName: manual, el mismo modo de acceso y un tamaño que quepa se enlaza con él, y el pod que lo use arranca en ese worker.
Ojo con
hostPathsinnodeAffinity: el pod podría arrancar en otro nodo,DirectoryOrCreatecrearía allí una carpeta vacía y parecería que los datos se han perdido.
Lo interesante llega al borrar el PVC: con Retain, el PV no se borra, queda en Released y los datos siguen en el nodo. Si volvemos a crear el mismo PVC, se queda en Pending: un PV Released sigue reservado para el PVC antiguo a través de su campo claimRef, aunque ese PVC ya no exista. Para reutilizarlo hay que liberarlo a mano:
kubectl get pv pv-manual # Released
kubectl patch pv pv-manual --type=json -p='[{"op":"remove","path":"/spec/claimRef"}]'
kubectl get pvc datos-manual # Bound
Es justo lo que busca Retain: que una persona decida qué hacer con los datos antes de reutilizar el disco. Borrarlos del nodo también es cosa nuestra.
Longhorn: volúmenes replicados entre nodos
Con local-path y con el PV manual, si el nodo cae, el pod no puede arrancar en otro. Longhorn guarda cada volumen replicado en varios nodos: si cae uno, el pod arranca en otro y sus datos le esperan allí.
Su instalación tiene sus trampas (iSCSI, multipathd, número de réplicas y anti-afinidad con solo dos workers), así que la vemos paso a paso en almacenamiento avanzado.
ResourceQuota para limitar almacenamiento
Podemos restringir cuánto almacenamiento pide un namespace y cuántos PVC puede crear.
Limitar tamaño total y número de PVC
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-almacenamiento
namespace: demo
spec:
hard:
requests.storage: 50Gi # Suma de todos los PVC requests
persistentvolumeclaims: "10" # Número máximo de PVC
Limitar por StorageClass específica
Algunas versiones de Kubernetes permiten usar recursos específicos tipo: storageclass.storage.k8s.io/<nombre>.requests.storage.
apiVersion: v1
kind: ResourceQuota
metadata:
name: cuota-nfs-clase
namespace: demo
spec:
hard:
storageclass.storage.k8s.io/nfs-csi.requests.storage: 20Gi
Si el cluster no soporta el recurso específico, valida con
kubectl api-resources | grep storageclass.storage.k8s.io.
Verificar uso
kubectl describe quota cuota-almacenamiento -n demo
Buenas Prácticas
- Definamos una StorageClass por perfil (rendimiento, backup, criptografía).
- Usemos
WaitForFirstConsumeren entornos multi-zona para evitar volúmenes en zonas erróneas. - Apliquemos etiquetas en PV estáticos para control fino de binding.
- Documentemos políticas de
Retainpara evitar acumulación de volúmenes huérfanos. - Monitoricemos el estado de PVC (
Pending) como indicador de problemas de aprovisionamiento. - Usemos
StatefulSetpara aplicaciones que requieren volúmenes persistentes por réplica.
Checklist de Decisiones Rápidas
| Pregunta | Si la respuesta es... | Acción |
|---|---|---|
| ¿Necesito flexibilidad y escalado? | Sí | StorageClass + CSI |
| ¿Es un entorno de pruebas fijo? | Sí | PV estático |
| ¿Necesito compartir datos entre Pods/Nodos? | Sí | RWX (NFS, CephFS) |
| ¿Necesito máximo rendimiento local? | Sí | RWO (discos locales / cloud SSD) |
| ¿Necesito snapshots o expansión? | Sí | Ver capacidades del CSI driver |
Resumen
Kubernetes abstrae el almacenamiento con PV (recurso), PVC (petición), StorageClass (plantilla/parametrización) y CSI (implementación estándar). El aprovisionamiento estático exige más gestión manual y se orienta a entornos controlados; el dinámico acelera la entrega y reduce intervención operativa. Elegir correctamente la clase, política de reclamación y modo de acceso evita cuellos de botella y sorpresas en producción.
- Lista de vídeos en Youtube: Curso Kubernetes
