Saltar al contenido principal

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:

  1. La infraestructura física o lógica (discos, NFS, Ceph, EBS, etc.)
  2. Las necesidades de las aplicaciones (tamaño, modo de acceso, rendimiento)
  3. 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

Curso Kubernetes

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 principalEstáticoDinámico
PVOperador / AdminCuando se quiere aprovisionar manualmenteRepresenta almacenamiento físico/lógicoSíReferenciado por StorageClass (indirecto)
PVCDesarrollador / AppCuando la app necesita almacenamientoSolicita y vincula almacenamientoSí (claim a PV existente)Sí (dispara creación vía StorageClass)
StorageClassOperadorDefine parámetros de aprovisionamiento dinámicoPlantilla de volúmenesNo aplica directamenteSí
CSI DriverProveedorInstalado en clusterImplementa operaciones de volumenHabilita PV pre-creadosHabilita creación dinámica

Provisionamiento Estático vs Dinámico​

  1. La aplicación crea un PVC solicitando tamaño y modo de acceso.
  2. Kubernetes intenta vincular el PVC:
    • Si especifica storageClassName, se crea un PV dinámicamente usando el CSI driver asociado a esa StorageClass.
    • Si NO especifica storageClassName y 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, volumeMode y sin clase).
  3. El PVC se vincula (Bound) y el Pod monta el volumen.
  4. Al eliminar el PVC, la acción sobre el PV depende de persistentVolumeReclaimPolicy (Recycle, Retain, Delete — hoy Recycle está 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: true y se actualiza spec.resources.requests.storage del 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?​

EscenarioRecomendación
Test rápido / laboratorioPV estático sencillo (NFS)
Multi-tenant con distintos patronesStorageClass con distintos parámetros (gold, silver, backup)
Cloud con discos gestionadosAprovisionamiento dinámico (EBS / PD / etc.)
Rendimiento crítico (BD)StorageClass específica (IOPS alto, volumenMode Block)
Migraciones heredadasPV 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-pathPV manualLonghorn
Quién crea el volumenUn provisionador, al pedirloNosotros, a manoUn provisionador, al pedirlo
Dónde viven los datosEn el disco de un nodoEn el disco de un nodoReplicados en varios nodos
Si cae ese nodoEl pod no puede moverseEl pod no puede moverseEl 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 hostPath sin nodeAffinity: el pod podría arrancar en otro nodo, DirectoryOrCreate crearí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​

  1. Definamos una StorageClass por perfil (rendimiento, backup, criptografía).
  2. Usemos WaitForFirstConsumer en entornos multi-zona para evitar volúmenes en zonas erróneas.
  3. Apliquemos etiquetas en PV estáticos para control fino de binding.
  4. Documentemos políticas de Retain para evitar acumulación de volúmenes huérfanos.
  5. Monitoricemos el estado de PVC (Pending) como indicador de problemas de aprovisionamiento.
  6. Usemos StatefulSet para aplicaciones que requieren volúmenes persistentes por réplica.

Checklist de Decisiones Rápidas​

PreguntaSi 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.


Volver al índice