La lección anterior terminó con un PersistentVolume que apareció solo, y con una caja negra sin abrir: dijimos "el aprovisionador crea el volumen" sin explicar quién es ese aprovisionador, cómo se entera de que hay un PVC pendiente, cómo conecta el disco a un nodo y cómo acaba montado en el contenedor. Esa maquinaria se llama CSI, la Container Storage Interface, y entenderla es lo que separa a quien aplica manifiestos de quien puede diagnosticar por qué un pod lleva veinte minutos en ContainerCreating. Además, la StorageClass anunciaba dos capacidades que aún no hemos usado: la expansión de un volumen en marcha y los snapshots, imprescindibles antes de tocar el esquema de una base de datos en producción. En esta lección las pondrás a trabajar sobre postgres-reservas, y cerrarás con la advertencia que más incidentes evita: un snapshot no es una copia de seguridad.
Contenido
- Qué problema resolvió CSI
- La arquitectura: plugin de controlador y plugin de nodo
- Los sidecars y los objetos
CSIDriveryCSINode - El flujo completo de un volumen, de extremo a extremo
- Preparar el clúster de prácticas
- Expansión de volúmenes: requisitos y mecánica
- Expansión en línea frente a expansión con reinicio
- Snapshots: los tres objetos
- Crear un snapshot de
postgres-reservas - Restaurar desde un snapshot con
dataSource - Clonar un PVC
- La advertencia: consistencia y qué NO es un snapshot
- Qué problema resolvió CSI
Antes de 2018, el código que hablaba con AWS EBS, con Google Persistent Disk, con Ceph o con NetApp vivía dentro del repositorio de Kubernetes. Se les llamaba controladores in-tree, y el modelo tenía tres problemas graves:
| Problema | Consecuencia |
|---|---|
| El código del proveedor iba en Kubernetes | Un fallo en el driver de un fabricante podía tumbar el kube-controller-manager |
| Los ciclos de publicación iban acoplados | Corregir un error exigía esperar a una versión de Kubernetes, y cada mejora pasaba por la revisión del proyecto |
| Las credenciales del proveedor vivían en el plano de control | Superficie de ataque considerable |
CSI (Container Storage Interface) es una especificación estándar —no exclusiva de Kubernetes: la usan también Mesos y Nomad— que define un contrato gRPC entre el orquestador y el sistema de almacenamiento. El proveedor implementa ese contrato en un componente propio, lo empaqueta como contenedor y lo despliega en el clúster, como cualquier otra carga de trabajo. El resultado: los controladores in-tree quedaron obsoletos y fueron eliminados (kubernetes.io/aws-ebs, kubernetes.io/gce-pd, kubernetes.io/azure-disk ya no existen en 1.30+). Hoy todo el almacenamiento de red pasa por CSI, y hay más de un centenar de drivers disponibles.
- La arquitectura: plugin de controlador y plugin de nodo
Un driver CSI se despliega siempre en dos mitades, porque las operaciones de almacenamiento son de dos naturalezas distintas:
flowchart TB
subgraph CP["Plano de control del cluster"]
API["kube-apiserver"]
subgraph CTRLPOD["Plugin de CONTROLADOR (Deployment, 1-2 replicas)"]
SC1["sidecars: external-provisioner,<br/>-attacher, -resizer, -snapshotter"]
DRVC["Driver CSI<br/>(logica del proveedor)"]
end
end
subgraph N1["Nodo 1"]
K1["kubelet"]
subgraph NODEPOD["Plugin de NODO (DaemonSet, en CADA nodo)"]
REG["node-driver-registrar"]
DRVN["Driver CSI<br/>(operaciones locales)"]
end
end
NUBE[("API del proveedor<br/>de almacenamiento")]
API <--> SC1
SC1 <-->|"gRPC por socket UNIX"| DRVC
DRVC <--> NUBE
K1 <-->|"gRPC por socket UNIX"| DRVN
REG -->|"registra el driver"| K1
DRVN --> DISCO["Formatear y montar<br/>en el sistema de ficheros del nodo"]
| Plugin de controlador | Plugin de nodo | |
|---|---|---|
| Se despliega como | Deployment (1 o 2 réplicas) |
DaemonSet (uno por nodo, 06-02) |
| Habla con | La API del proveedor de almacenamiento | El sistema de ficheros del nodo |
| Operaciones | CreateVolume, DeleteVolume, ControllerPublishVolume, CreateSnapshot, ControllerExpandVolume |
NodeStageVolume, NodePublishVolume, NodeExpandVolume |
| En cristiano | "Crea un disco de 20 GiB y conéctalo a la máquina 3" | "Formatea ese disco y móntalo en esta ruta" |
| Necesita credenciales de la nube | Sí | No |
La separación no es caprichosa: crear un disco es una llamada a una API remota que solo debe hacerse una vez desde un sitio; montarlo es una operación local que solo puede hacer la máquina donde está el pod.
- Los sidecars y los objetos
CSIDriver y CSINode
CSIDriver y CSINodeAquí está la parte elegante del diseño. El driver del proveedor no habla con la API de Kubernetes: solo implementa el contrato gRPC de CSI. Quien observa la API y traduce son unos contenedores auxiliares mantenidos por el proyecto Kubernetes, los sidecars, que se despliegan junto al driver en el mismo pod (el patrón multi-contenedor de 06-04).
| Sidecar | Qué observa en la API | Qué llama en el driver |
|---|---|---|
external-provisioner |
PVC pendientes de una clase suya | CreateVolume / DeleteVolume |
external-attacher |
Objetos VolumeAttachment |
ControllerPublishVolume |
external-resizer |
PVC cuyo requests.storage ha crecido |
ControllerExpandVolume |
external-snapshotter |
Objetos VolumeSnapshot |
CreateSnapshot / DeleteSnapshot |
node-driver-registrar |
— | Registra el driver ante el kubelet del nodo |
Hay además un livenessprobe que sondea la salud del driver. Gracias a este diseño, un fabricante solo escribe la lógica de su almacenamiento; toda la integración con Kubernetes ya está hecha y probada. Y hay dos objetos de la API que describen el estado de esta infraestructura:
CSIDriver
Declara las capacidades de un driver instalado. El clúster lo consulta para saber qué puede pedirle.
Name: hostpath.csi.k8s.io
Spec:
Attach Required: true # hace falta la fase de conexion?
Fs Group Policy: File # como se aplica fsGroup (ver 05-03)
Pod Info On Mount: true # el driver recibe datos del pod al montar
Volume Lifecycle Modes: Persistent, EphemeralCSINode
Se crea automáticamente por cada nodo y registra qué drivers hay disponibles allí y con qué identificador conoce el proveedor a esa máquina.
Name: rutas-norte
Spec:
Drivers:
hostpath.csi.k8s.io:
Node ID: rutas-norte
Allocatables:
Count: 10 # maximo de volumenes por nodo
Topology Keys: [topology.hostpath.csi/node]Ese Count es una limitación muy real en producción: AWS, por ejemplo, restringe el número de volúmenes EBS conectables a una instancia. Cuando se agota, los pods se quedan Pending con node(s) exceed max volume count, un mensaje que ahora sabes de dónde sale.
- El flujo completo de un volumen, de extremo a extremo
Este es el diagrama que hay que retener: qué ocurre exactamente desde que aplicas un PVC hasta que el proceso escribe su primer byte.
sequenceDiagram
participant U as Tu (kubectl apply)
participant API as apiserver
participant PRO as external-provisioner
participant DRV as Driver CSI (controlador)
participant NUBE as API del proveedor
participant SCH as Planificador
participant ATT as external-attacher
participant KBL as kubelet del nodo
participant DVN as Driver CSI (nodo)
U->>API: PVC (clase rutasnorte-rapida)
Note over API: PVC Pending (WaitForFirstConsumer)
U->>API: Deployment postgres-reservas
SCH->>API: pod asignado al nodo-2
PRO->>DRV: CreateVolume(20Gi, zona del nodo-2)
DRV->>NUBE: crear disco
NUBE-->>DRV: volumeHandle vol-0a1b2c3d
PRO->>API: crea el PV y lo vincula (Bound)
ATT->>API: crea VolumeAttachment
ATT->>DRV: ControllerPublishVolume(vol, nodo-2)
DRV->>NUBE: conectar el disco a la instancia
KBL->>DVN: NodeStageVolume
Note over DVN: formatea (ext4) y monta en el<br/>directorio global del nodo
KBL->>DVN: NodePublishVolume
Note over DVN: bind-mount al directorio del pod<br/>y aplica fsGroup
KBL->>API: pod Running
Las cinco fases, con su nombre técnico y el síntoma de cuando fallan:
| Fase | Quién | Si falla, verás |
|---|---|---|
| Provision | external-provisioner + controlador |
PVC Pending, evento ProvisioningFailed |
| Attach | external-attacher + controlador |
Pod ContainerCreating, evento FailedAttachVolume, Multi-Attach error |
| NodeStage | kubelet + plugin de nodo | FailedMount, errores de formateo o de opciones de montaje |
| NodePublish | kubelet + plugin de nodo | FailedMount, problemas de permisos o fsGroup |
| Unpublish → Delete | Los mismos, en orden inverso | PVC en Terminating, volúmenes huérfanos |
Dos detalles que valen oro en un diagnóstico. NodeStageVolume ocurre una vez por nodo; NodePublishVolume, una vez por pod, y por eso un volumen puede estar "montado" y aun así el pod no verlo: son dos montajes distintos. Y puedes seguir la fase de conexión en la API con kubectl get volumeattachments, algo que rara vez se sabe: muestra una fila por volumen conectado, con el driver, el PV, el nodo y una columna ATTACHED; si esa columna es false durante minutos, el problema está en el plugin de controlador, no en el nodo.
- Preparar el clúster de prácticas
El provisioner de minikube (k8s.io/minikube-hostpath) no admite expansión ni snapshots, así que para esta lección instalaremos el driver CSI de hostpath, que sí las implementa. Es un driver de referencia para pruebas —no lo uses en producción— pero reproduce fielmente el comportamiento de un CSI real.
# 1. Los CRDs de snapshots y el snapshot-controller (NO vienen con Kubernetes)
VER=v8.1.0
BASE=https://raw.githubusercontent.com/kubernetes-csi/external-snapshotter/$VER
for f in client/config/crd/snapshot.storage.k8s.io_volumesnapshotclasses.yaml \
client/config/crd/snapshot.storage.k8s.io_volumesnapshotcontents.yaml \
client/config/crd/snapshot.storage.k8s.io_volumesnapshots.yaml \
deploy/kubernetes/snapshot-controller/rbac-snapshot-controller.yaml \
deploy/kubernetes/snapshot-controller/setup-snapshot-controller.yaml; do
kubectl apply -f "$BASE/$f"
done
# 2. El driver CSI de hostpath
minikube addons enable csi-hostpath-driver -p rutas-norte
minikube addons enable volumesnapshots -p rutas-norte
# 3. Comprobar
kubectl get csidrivers
kubectl get sc
kubectl get volumesnapshotclassesNAME ATTACHREQUIRED MODES AGE
hostpath.csi.k8s.io true Persistent,Ephemeral 1m
NAME PROVISIONER RECLAIMPOLICY ALLOWVOLUMEEXPANSION
csi-hostpath-sc hostpath.csi.k8s.io Delete true
NAME DRIVER DELETIONPOLICY AGE
csi-hostpath-snapclass hostpath.csi.k8s.io Delete 1mPunto importante: los CRDs de snapshot y el snapshot-controller no forman parte de Kubernetes. En un clúster gestionado suelen venir instalados, pero en uno propio hay que ponerlos. Si intentas crear un VolumeSnapshot sin ellos, el error es no matches for kind "VolumeSnapshot" in version "snapshot.storage.k8s.io/v1".
Ahora reescribimos rutasnorte-rapida sobre este driver —hay que borrarla y recrearla, porque las StorageClass no se editan—, manteniendo el contrato (nombre y Retain) y ganando las dos capacidades:
# k8s/entornos/dev/storageclass-rapida-csi.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rutasnorte-rapida
labels: { app.kubernetes.io/part-of: rutas-norte }
provisioner: hostpath.csi.k8s.io # driver CSI de pruebas
reclaimPolicy: Retain
allowVolumeExpansion: true # <-- ahora SI
volumeBindingMode: Immediate
- Expansión de volúmenes: requisitos y mecánica
Llega el día en que 10 GiB se quedan cortos: la tabla de reservas ocupa 8,4 GiB y el disco va camino de llenarse en dos semanas. La expansión permite agrandar el volumen sin migrar los datos.
Los requisitos, los tres a la vez: la StorageClass debe tener allowVolumeExpansion: true, el driver CSI debe implementar la capacidad (ControllerExpandVolume) y el PVC debe estar Bound.
Y la restricción absoluta: un volumen no se puede reducir. Ni con un PVC nuevo, ni con kubectl edit, ni de ninguna manera; Kubernetes rechaza la petición con spec.resources.requests.storage: Forbidden: field can not be less than previous value.
La razón es de seguridad de datos: reducir un sistema de ficheros exige moverlo y compactarlo, y ningún proveedor garantiza hacerlo sin riesgo. Consecuencia práctica: pide con cabeza, porque solo puedes subir. Reducir significa crear un PVC menor y copiar, con parada, como en 05-04.
Cómo se pide
Se edita spec.resources.requests.storage del PVC —nunca del PV—. La vía rápida es un patch; la buena, editar el manifiesto en Git y aplicarlo, como manda el modelo declarativo de 01-06:
kubectl get pvc postgres-reservas-datos -n rutas-norte-dev # CAPACITY: 10Gi
kubectl patch pvc postgres-reservas-datos -n rutas-norte-dev \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# Se sigue el proceso por las CONDICIONES del PVC
kubectl describe pvc postgres-reservas-datos -n rutas-norte-dev | tail -10Conditions:
Type Status Message
Resizing True Waiting for user to (re-)start a pod
Events:
Normal Resizing external-resizer hostpath.csi.k8s.io External resizer is resizing volume
Normal FileSystemResizeSuccessful kubelet MountVolume.NodeExpandVolume succeededY la comprobación que de verdad importa, dentro del contenedor:
kubectl get pvc postgres-reservas-datos -n rutas-norte-dev # CAPACITY: 20Gi
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- df -h /var/lib/postgresql/data
# /dev/vdb 20G 8.4G 11G 45% /var/lib/postgresql/data20 GiB, con los 8,4 GiB de datos intactos y sin haber parado la base de datos.
- Expansión en línea frente a expansión con reinicio
La expansión ocurre en dos etapas, y de ahí las dos condiciones que verás en el PVC:
flowchart LR
A["Editas el PVC:<br/>10Gi -> 20Gi"] --> B["external-resizer<br/>ControllerExpandVolume"]
B --> C["El disco REAL pasa a 20Gi<br/>(condicion: Resizing)"]
C --> D{"Expansion<br/>en linea?"}
D -->|"Si"| E["kubelet: NodeExpandVolume<br/>resize2fs sobre el FS montado"]
D -->|"No"| F["FileSystemResizePending<br/>-> RECREAR el pod"] --> E
E --> H["df -h dentro del contenedor<br/>muestra 20Gi"]
| Expansión en línea | Expansión con reinicio | |
|---|---|---|
| El pod sigue corriendo | Sí | No: hay que recrearlo |
| Condición que aparece | Resizing y desaparece |
FileSystemResizePending persistente |
| Soporte | La mayoría de los drivers CSI modernos | Drivers antiguos, algunos casos con volumeMode: Block |
| Acción necesaria | Ninguna | kubectl rollout restart |
Si el PVC se queda con la condición FileSystemResizePending, el disco ya es mayor pero el sistema de ficheros no lo sabe: df -h dentro del contenedor sigue mostrando el tamaño antiguo. La solución es recrear el pod, y el kubelet ejecuta NodeExpandVolume durante el montaje:
kubectl get pvc postgres-reservas-datos -n rutas-norte-dev \
-o jsonpath='{.status.conditions[*].type}'; echo # -> FileSystemResizePending
# postgres-reservas usa Recreate: habra un corte breve. Avisa antes.
kubectl rollout restart deploy/postgres-reservas -n rutas-norte-dev
kubectl rollout status deploy/postgres-reservas -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- df -h /var/lib/postgresql/dataDos avisos operativos. Primero, subPath puede impedir la expansión automática del sistema de ficheros en algunos drivers: es la razón, anunciada en 05-03, por la que en postgres-reservas preferimos la variable PGDATA al subPath. Y segundo, vigila el llenado antes de que sea urgente: Kubernetes expone kubelet_volume_stats_available_bytes y kubelet_volume_stats_capacity_bytes, y una alerta al 75 % da margen de sobra (07-03 y 07-04).
- Snapshots: los tres objetos
Un snapshot es una copia puntual del contenido de un volumen, hecha por el sistema de almacenamiento. Su gran virtud es que es casi instantánea —los sistemas modernos usan copia sobre escritura, así que no duplican los datos al crearla— y que permite crear volúmenes nuevos a partir de ella. El modelo de objetos es un calco exacto del de los volúmenes, lo que hace que ya te lo sepas:
| Volúmenes | Snapshots | Papel |
|---|---|---|
StorageClass |
VolumeSnapshotClass |
Qué driver y con qué política (recurso de clúster) |
PersistentVolumeClaim |
VolumeSnapshot |
La petición del usuario (con namespace) |
PersistentVolume |
VolumeSnapshotContent |
El objeto que representa el snapshot real (recurso de clúster) |
flowchart LR
subgraph NS["namespace rutas-norte-dev"]
PVC["PersistentVolumeClaim<br/>postgres-reservas-datos"]
VS["VolumeSnapshot<br/>snap-antes-migracion"]
end
subgraph CL["Recursos de cluster"]
PV["PersistentVolume"]
VSC["VolumeSnapshotContent<br/>snapcontent-a1b2..."]
VSCLASS["VolumeSnapshotClass<br/>rutasnorte-snapclass"]
end
PVC --> PV
VS -->|"source"| PVC
VS --> VSC
VSC -.->|"clase"| VSCLASS
VSC -->|"vive en el MISMO<br/>sistema de almacenamiento"| PV
Y la VolumeSnapshotClass, análoga a la StorageClass:
# k8s/base/volumesnapshotclass.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: rutasnorte-snapclass
labels: { app.kubernetes.io/part-of: rutas-norte }
driver: hostpath.csi.k8s.io # debe coincidir con el de la StorageClass
deletionPolicy: Retain # Retain o Delete, como en los PVdeletionPolicy decide qué ocurre con el snapshot real al borrar el objeto VolumeSnapshot: Delete lo destruye; Retain lo conserva, y es la elección coherente con rutasnorte-rapida para los snapshots de la base de datos.
Requisitos previos, que ya cubriste en el apartado 5: los tres CRDs, el snapshot-controller y el sidecar external-snapshotter junto al driver.
- Crear un snapshot de
postgres-reservas
postgres-reservasEl escenario real: mañana se despliega la versión 3.0 de api-reservas, que incluye una migración de esquema —añade columnas a reservas y reescribe la tabla de clientes—. Si la migración sale mal, hay que volver atrás en minutos, y un rollout undo del Deployment (02-04) no deshace los cambios en la base de datos. Un snapshot previo sí.
Estado de partida: tres reservas en la tabla, según kubectl exec ... psql -c "SELECT count(*) FROM reservas;". El manifiesto del snapshot:
# k8s/entornos/dev/snapshot-previo-migracion.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: snap-postgres-previo-v3
namespace: rutas-norte-dev
labels: { app: postgres-reservas, app.kubernetes.io/part-of: rutas-norte, entorno: dev }
annotations:
rutasnorte.example/motivo: "previo a la migracion de esquema de api-reservas 3.0.0"
rutasnorte.example/responsable: "[email protected]"
rutasnorte.example/ticket: "RN-874"
spec:
volumeSnapshotClassName: rutasnorte-snapclass
source:
persistentVolumeClaimName: postgres-reservas-datos # el PVC a fotografiarkubectl apply -f k8s/base/volumesnapshotclass.yaml
kubectl apply -f k8s/entornos/dev/snapshot-previo-migracion.yaml
kubectl get volumesnapshot -n rutas-norte-devNAME READYTOUSE SOURCEPVC RESTORESIZE SNAPSHOTCONTENT AGE
snap-postgres-previo-v3 true postgres-reservas-datos 20Gi snapcontent-a1b2c3d4-... 10sREADYTOUSE: true es la columna que hay que mirar: hasta que no lo sea, el snapshot no sirve para restaurar. El describe amplía con el Bound Volume Snapshot Content Name, la Creation Time, el Restore Size y los eventos CreatingSnapshot y SnapshotCreated del snapshot-controller.
Las anotaciones con el motivo, el responsable y el ticket no son adorno: un clúster acumula snapshots, cada uno cuesta dinero, y sin esa información nadie se atreve a borrar ninguno. Es la aplicación directa de lo aprendido en 02-07.
- Restaurar desde un snapshot con
dataSource
dataSourceAhora simulamos el desastre. La migración se ejecuta y va mal: borra reservas.
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
psql -U rutasnorte -d reservas -c "DELETE FROM reservas WHERE id > 1;
SELECT count(*) FROM reservas;" # -> 1La restauración no sobrescribe el volumen original: crea un PVC nuevo cuyo dataSource apunta al snapshot. Es una diferencia crucial, porque conserva la evidencia del estado dañado para poder analizarlo después.
# k8s/entornos/dev/pvc-restaurado.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-reservas-datos-restaurado
namespace: rutas-norte-dev
labels: { app: postgres-reservas, app.kubernetes.io/part-of: rutas-norte, entorno: dev }
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida # la MISMA clase del origen
resources: { requests: { storage: 20Gi } } # >= restoreSize del snapshot
dataSource:
name: snap-postgres-previo-v3
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.ioAl aplicarlo aparece un segundo PVC Bound junto al original, con su propio PV. Y apuntamos el Deployment al volumen restaurado:
kubectl apply -f k8s/entornos/dev/pvc-restaurado.yaml
kubectl scale deploy postgres-reservas -n rutas-norte-dev --replicas=0
kubectl patch deploy postgres-reservas -n rutas-norte-dev --type=json -p='[
{"op":"replace",
"path":"/spec/template/spec/volumes/0/persistentVolumeClaim/claimName",
"value":"postgres-reservas-datos-restaurado"}]'
kubectl scale deploy postgres-reservas -n rutas-norte-dev --replicas=1
kubectl rollout status deploy/postgres-reservas -n rutas-norte-dev
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
psql -U rutasnorte -d reservas -c "SELECT * FROM reservas;" id | cliente | trayecto | fecha
----+-----------------+-------------------+------------
1 | Marta Iglesias | Bilbao-Santander | 2026-08-15
2 | Ignacio Sabater | Oviedo-Gijon | 2026-08-16
3 | Lucia Berenguer | Leon-Ponferrada | 2026-08-17Las tres reservas están de vuelta. Cuatro condiciones para que una restauración funcione:
- El PVC nuevo debe pedir al menos el
restoreSizedel snapshot. Menos, y falla. - Debe usar la misma StorageClass (o al menos el mismo driver): un snapshot no cruza sistemas de almacenamiento.
- El snapshot debe estar
READYTOUSE: true. - El snapshot debe existir en el mismo namespace que el PVC nuevo. Para cruzar namespaces hay que importar el
VolumeSnapshotContenta mano.
- Clonar un PVC
Un caso hermano y muy útil: crear una copia de un volumen directamente desde otro PVC, sin pasar por un snapshot. Cambia solo el kind del dataSource:
# k8s/entornos/dev/pvc-clon-para-pruebas.yaml (mismo esquema que el restaurado)
metadata:
name: postgres-reservas-datos-clon-qa
annotations:
rutasnorte.example/motivo: "clon para probar la migracion 3.0 sin tocar dev"
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: rutasnorte-rapida
resources: { requests: { storage: 20Gi } }
dataSource:
name: postgres-reservas-datos # OTRO PVC, no un snapshot
kind: PersistentVolumeClaim # <-- lo unico que cambia| Restaurar de snapshot | Clonar un PVC | |
|---|---|---|
dataSource.kind |
VolumeSnapshot |
PersistentVolumeClaim |
| Origen | Una foto de un momento pasado | El estado actual del volumen |
| Requiere snapshot previo | Sí | No |
| Namespace | El mismo | El mismo |
| Uso típico | Recuperar de un desastre | Montar un entorno de pruebas con datos reales |
El clonado es la forma limpia de dar al equipo de desarrollo una copia de los datos de preproducción para ensayar una migración. Y ojo con la letra pequeña: ese clon contiene datos personales reales de clientes —nombre, DNI, teléfono, correo—. Un clon para pruebas es un tratamiento de datos igual que el original y exige las mismas protecciones, o mejor, una anonimización previa. Se retoma en 05-06.
Una limitación técnica que conviene saber: clonar un PVC mientras la base de datos está escribiendo produce una copia equivalente a la de un corte de luz, con los mismos problemas de consistencia del apartado siguiente.
- La advertencia: consistencia y qué NO es un snapshot
Un snapshot de disco puede no ser consistente
Esta es la advertencia central de la lección. Un snapshot fotografía los bloques del disco, no el estado lógico de la aplicación. Y una base de datos en marcha tiene, en cualquier instante, páginas modificadas en memoria que aún no se han escrito al disco, transacciones a medio confirmar, y escrituras en el WAL que el sistema operativo tiene en su caché y no ha volcado.
El snapshot resultante equivale a el estado del disco tras un corte de corriente. La buena noticia es que PostgreSQL está preparado para eso: al arrancar sobre ese volumen ejecuta una recuperación por WAL y llega a un estado coherente. La mala es doble: esa recuperación puede tardar minutos en una base grande, y perderás las transacciones que no llegaron al disco. Y con motores menos robustos, o con datos repartidos entre varios volúmenes que se fotografían por separado, el resultado puede ser directamente inservible. La solución profesional es coordinar el snapshot con la aplicación: dejarla en un estado consistente justo antes, y liberarla justo después.
| Nivel de consistencia | Cómo se consigue | Riesgo |
|---|---|---|
| Crash-consistent | Snapshot sin más | Recuperación por WAL al arrancar; pérdida de lo no volcado |
| Filesystem-consistent | sync y congelar el sistema de ficheros (fsfreeze) antes |
Bajo |
| Application-consistent | La aplicación entra en modo copia (pg_backup_start / pg_backup_stop) |
Mínimo |
Kubernetes ofrece el mecanismo para lo tercero: los hooks de pre y post que ejecutan un comando dentro del contenedor antes y después del snapshot. No es una capacidad nativa del VolumeSnapshot, sino de las herramientas de copia de seguridad, y por eso se estudia en la lección siguiente, 05-06, con Velero.
Un snapshot NO es una copia de seguridad
Es la afirmación más importante del módulo y el error conceptual que más datos ha destruido:
| Snapshot | Copia de seguridad | |
|---|---|---|
| Dónde vive | En el mismo sistema de almacenamiento que el original | En un sistema independiente (almacén de objetos, otra región) |
| Sobrevive a perder la cabina, la zona o la cuenta | No | Sí (si está en otra cuenta) |
| Sobrevive a un cifrado por ransomware con credenciales robadas | Normalmente no | Sí, con inmutabilidad |
| Portable a otro clúster o proveedor | No | Sí |
| Tiempo de creación / coste | Segundos, coste bajo (incremental) | Minutos u horas, coste mayor |
| Incluye los manifiestos de Kubernetes | No: solo los datos del volumen | Sí, si la herramienta los captura |
Si se borra la cuenta de la nube, si la zona se pierde, si alguien con credenciales de administrador destruye el proyecto, los snapshots se van con todo lo demás. Son una herramienta magnífica para lo que son —deshacer un cambio local, rápido, en minutos—, pero no son una estrategia de recuperación ante desastres. Regla de Rutas Norte: los snapshots son la red de seguridad de corto plazo —antes de una migración, de un despliegue arriesgado, de tocar producción—, con retención de días; la copia de seguridad de verdad va a un almacén de objetos externo y es lo que se construye en la lección siguiente.
Errores Comunes y Consejos
| Error | Síntoma | Solución |
|---|---|---|
Crear un VolumeSnapshot sin CRDs ni controlador |
no matches for kind "VolumeSnapshot" |
Instala los CRDs y el snapshot-controller |
| Intentar reducir un PVC | field can not be less than previous value |
No se puede: copiar a un PVC menor con parada |
Expandir con allowVolumeExpansion: false |
El PVC no cambia y aparece un evento de rechazo | La clase se debe recrear con el campo a true |
| Editar el PV en vez del PVC para expandir | No pasa nada útil | La expansión se pide siempre en el PVC |
No mirar df -h dentro del contenedor |
Se cree que la expansión terminó | La condición FileSystemResizePending exige recrear el pod |
| Restaurar en un PVC más pequeño que el snapshot, o con otra clase | El PVC no se aprovisiona | Pide >= restoreSize, y misma clase y driver |
| Creer que un snapshot es una copia de seguridad | Pérdida total si cae el sistema de almacenamiento | Copia externa (05-06) |
| Snapshot de una BD en marcha sin coordinar | Recuperación larga o datos inservibles | Hooks pre/post, o volcado lógico |
| Snapshots sin anotaciones ni retención | Decenas de snapshots caros que nadie se atreve a borrar | Anota motivo, responsable y ticket; define retención |
| Clonar producción a un entorno de pruebas sin pensar | Datos personales de clientes en un entorno menos protegido | Anonimiza o aplica las mismas protecciones |
Consejos:
- Antes de cualquier cambio irreversible —migración de esquema, actualización mayor del motor, script de limpieza— haz un snapshot y anótalo con el ticket. Cuesta segundos y ha salvado carreras.
- Verifica que un snapshot sirve, no que existe. Un snapshot no probado no es nada. Restaura periódicamente en un PVC de prueba y comprueba las filas.
- Vigila el llenado con antelación con un
df -hsobre el punto de montaje, y automatízalo con alertas al 75 % en 07-04. - Diagnostica el almacenamiento por fases. Si un pod no arranca por el volumen, mira en este orden:
kubectl describe pvc(provisión),kubectl get volumeattachments(conexión),kubectl describe pod(montaje) y los registros del pod del driver CSI enkube-system.
Ejercicios
Ejercicio 1: expandir el volumen de la base de datos
Con el driver CSI de hostpath instalado y rutasnorte-rapida recreada con allowVolumeExpansion: true:
- Comprueba el tamaño actual del volumen de
postgres-reservasdesde dentro del contenedor. - Amplíalo de 10 GiB a 20 GiB editando el PVC.
- Sigue las condiciones del PVC hasta que termine y verifica el tamaño con
df -h. - Intenta reducirlo a 5 GiB y transcribe el error.
- Comprueba que las reservas siguen ahí.
Ejercicio 2: snapshot, desastre y restauración
Ensaya el procedimiento completo que usarás en producción:
- Inserta cinco reservas ficticias en
postgres-reservas. - Crea un
VolumeSnapshotllamadosnap-practicacon las anotaciones de motivo, responsable y ticket. Espera aREADYTOUSE: true. - Provoca el desastre:
DROP TABLE reservas. - Crea un PVC restaurado con
dataSourcey apunta el Deployment a él. - Verifica que las cinco reservas han vuelto.
- Mide el tiempo total desde el paso 3 hasta el 5 y anótalo: es tu RTO real para este tipo de incidente.
Ejercicio 3: diseñar la política de snapshots de Rutas Norte
Responde de forma razonada, en forma de tabla de decisión que puedas presentar al equipo:
- A. ¿Qué volúmenes de Rutas Norte merecen snapshots automáticos y cuáles no? Justifica cada componente.
- B. ¿Con qué frecuencia y con qué retención? Ten en cuenta que en un puente se venden unas 400 reservas por hora y que un snapshot de 20 GiB cuesta dinero cada mes.
- C. ¿Por qué los snapshots no bastan como estrategia de protección de datos de Rutas Norte? Enumera tres escenarios concretos en los que no salvarían la situación.
Soluciones
Ejercicio 1
# 1: parte de 9.8G, 2% usado
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- df -h /var/lib/postgresql/data
# 2
kubectl patch pvc postgres-reservas-datos -n rutas-norte-dev \
-p '{"spec":{"resources":{"requests":{"storage":"20Gi"}}}}'
# 3
kubectl get pvc postgres-reservas-datos -n rutas-norte-dev -w # Ctrl-C al ver 20Gi
kubectl get pvc postgres-reservas-datos -n rutas-norte-dev \
-o jsonpath='{range .status.conditions[*]}{.type}={.status}{"\n"}{end}'
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- df -h /var/lib/postgresql/dataEl df -h final debe decir 20G con solo un 1 % usado. Si status.conditions muestra FileSystemResizePending=True, falta la segunda etapa: un kubectl rollout restart deploy/postgres-reservas y ya lo dirá.
# 4
kubectl patch pvc postgres-reservas-datos -n rutas-norte-dev \
-p '{"spec":{"resources":{"requests":{"storage":"5Gi"}}}}'
# 5
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
psql -U rutasnorte -d reservas -c "SELECT count(*) FROM reservas;"The PersistentVolumeClaim "postgres-reservas-datos" is invalid:
spec.resources.requests.storage: Forbidden: field can not be less than previous valueLa expansión no toca los datos: solo agranda el dispositivo y extiende el sistema de ficheros sobre el espacio nuevo.
Ejercicio 2
# 1
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- psql -U rutasnorte -d reservas -c \
"TRUNCATE reservas;
INSERT INTO reservas (cliente, trayecto, fecha) VALUES
('Marta Iglesias','Bilbao-Santander','2026-08-15'),
('Ignacio Sabater','Oviedo-Gijon','2026-08-16'),
('Lucia Berenguer','Leon-Ponferrada','2026-08-17'),
('Ander Zuloaga','Vitoria-Burgos','2026-08-18'),
('Rosa Ferreiro','Lugo-A Coruna','2026-08-19');"# 2: snap-practica.yaml (el VolumeSnapshot del apartado 9, con otro nombre)
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: snap-practica
namespace: rutas-norte-dev
labels: { app: postgres-reservas, app.kubernetes.io/part-of: rutas-norte, entorno: dev }
annotations:
rutasnorte.example/motivo: "ensayo de restauracion del modulo 5"
rutasnorte.example/responsable: "[email protected]"
rutasnorte.example/ticket: "RN-901"
spec:
volumeSnapshotClassName: rutasnorte-snapclass
source: { persistentVolumeClaimName: postgres-reservas-datos }kubectl apply -f snap-practica.yaml
kubectl wait --for=jsonpath='{.status.readyToUse}'=true \
volumesnapshot/snap-practica -n rutas-norte-dev --timeout=180s
# 3: el desastre. A partir de aqui, cronometro.
INICIO=$(date +%s)
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
psql -U rutasnorte -d reservas -c "DROP TABLE reservas;"
# 4: el PVC restaurado del apartado 10, con dataSource -> snap-practica
kubectl apply -f k8s/entornos/dev/pvc-restaurado.yaml
kubectl scale deploy postgres-reservas -n rutas-norte-dev --replicas=0
kubectl patch deploy postgres-reservas -n rutas-norte-dev --type=json -p='[
{"op":"replace","path":"/spec/template/spec/volumes/0/persistentVolumeClaim/claimName",
"value":"postgres-reservas-datos-restaurado"}]'
kubectl scale deploy postgres-reservas -n rutas-norte-dev --replicas=1
kubectl rollout status deploy/postgres-reservas -n rutas-norte-dev
# 5 y 6
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
psql -U rutasnorte -d reservas -c "SELECT count(*) FROM reservas;"
echo "RTO real: $(( $(date +%s) - INICIO )) segundos"Ese número —del orden de uno a tres minutos en un clúster local, y de cinco a quince en producción con volúmenes grandes— es tu RTO para este tipo de incidente. Anótalo: es el dato que hay que llevar a la conversación sobre objetivos de recuperación de 05-06. Y fíjate en que el volumen original sigue ahí, con la tabla borrada, disponible para investigar qué pasó.
Ejercicio 3
A. Qué merece snapshots:
| Componente | ¿Snapshot? | Justificación |
|---|---|---|
postgres-reservas |
Sí, prioritario | Único origen de verdad de reservas y datos personales. Su pérdida para la venta |
| PVC de copias de seguridad | Sí | Contiene los volcados lógicos; perderlos deja sin red de seguridad |
redis-cache |
No | Reconstruible desde la base de datos. Un snapshot solo guardaría datos caducos |
tienda-web, api-reservas, worker-notificaciones |
No | Sin estado: su contenido está en la imagen y en Git, y la cola de pendientes vive en PostgreSQL |
Volumen de trabajo de informes-ocupacion |
No | Efímero por diseño; el informe se regenera |
B. Frecuencia y retención:
| Tipo | Frecuencia | Retención | Motivo |
|---|---|---|---|
| Programado nocturno | 1 al día (03:00) | 7 días | Cubre el error detectado a los pocos días. 7 snapshots de 20 GiB incrementales son baratos |
| Antes de cada cambio con riesgo | Bajo demanda | 72 h tras validar el cambio | Es su ventana útil: si a las 72 h no ha explotado, no explotará por ese cambio |
| Temporada alta (puentes, verano) | Cada 4 h | 48 h | A 400 reservas/hora, un snapshot diario implicaría perder hasta 9.600 reservas |
La aritmética que justifica el punto tres: con snapshot diario, el RPO es de 24 h y en un puente eso son unas 9.600 reservas perdidas. Con snapshot cada 4 h, el RPO baja a 4 h, es decir, unas 1.600. Y con el WAL archivado de forma continua bajaría a minutos, que es la solución de verdad y se comenta en 05-06. El RPO es una decisión de negocio, no técnica: alguien tiene que decidir cuántas reservas es aceptable perder.
C. Tres escenarios en los que los snapshots no salvan la situación:
- Pérdida del sistema de almacenamiento o de la zona. Los snapshots viven en la misma cabina o región que los volúmenes originales. Si desaparece, desaparecen con ella. Una copia de seguridad en un almacén de objetos de otra región sí sobrevive.
- Borrado de la cuenta o el proyecto de la nube, accidental o malicioso. Un administrador comprometido, o un error de facturación que suspende la cuenta, se lleva volúmenes y snapshots por igual. La protección es una copia en otra cuenta, con credenciales distintas e inmutabilidad activada.
- Corrupción lógica no detectada a tiempo. Un error de la aplicación que corrompe registros poco a poco durante tres semanas hace inútiles los snapshots de 7 días: todos contienen ya la corrupción. Hacen falta copias con retención larga —mensuales, anuales— y, sobre todo, volcados lógicos verificables: un
pg_dumpque se restaura correctamente demuestra que los datos son coherentes, cosa que un snapshot de bloques no demuestra. Un cuarto escenario que conviene mencionar: migrar a otro proveedor o restaurar en otro clúster; un snapshot de EBS no se restaura en GKE, un volcado lógico sí.
Conclusión
Has abierto la caja negra. Sabes que CSI existe porque tener el código de cada fabricante dentro de Kubernetes era insostenible, y que su arquitectura son dos mitades: un plugin de controlador, desplegado como Deployment, que habla con la API del proveedor y crea, conecta y fotografía volúmenes; y un plugin de nodo, desplegado como DaemonSet, que formatea y monta en cada máquina. Entre el driver y la API de Kubernetes se interponen los sidecars —external-provisioner, external-attacher, external-resizer, external-snapshotter, node-driver-registrar—, que traducen objetos de la API en llamadas gRPC y permiten que un fabricante solo escriba la lógica de su almacenamiento. Y conoces los objetos que describen esa infraestructura: CSIDriver, con las capacidades declaradas, y CSINode, con los drivers de cada nodo y ese Count que explica el exceed max volume count. Sobre todo, tienes el flujo completo en la cabeza —Provision, Attach, NodeStage, NodePublish— y sabes qué síntoma produce cada fase al fallar, incluido el kubectl get volumeattachments que casi nadie usa y que distingue en un segundo un problema de controlador de uno de nodo.
Has expandido en caliente el volumen de postgres-reservas de 10 a 20 GiB, con la base de datos en marcha y sin tocar un solo dato. Conoces los tres requisitos —allowVolumeExpansion, soporte del driver y PVC Bound—, que la petición se hace siempre en el PVC y nunca en el PV, y las dos etapas del proceso: la del disco real y la del sistema de ficheros, con la condición FileSystemResizePending que delata cuándo hace falta recrear el pod para que df -h diga la verdad. Y tienes grabada la restricción: no se puede reducir, así que la talla se piensa antes.
Y dominas los snapshots, con su modelo de objetos calcado del de los volúmenes —VolumeSnapshotClass, VolumeSnapshot, VolumeSnapshotContent, exactamente el papel de StorageClass, PVC y PV—, sus requisitos previos que no vienen con Kubernetes (los CRDs y el snapshot-controller), y el flujo de trabajo real: snapshot anotado con motivo, responsable y ticket antes de una migración de esquema; desastre; y restauración creando un PVC nuevo con dataSource, que no sobrescribe el original y conserva la evidencia. Sabes clonar un PVC en caliente cambiando el kind del dataSource, y que ese clon de producción lleva datos personales reales y merece el mismo cuidado que el original.
Y te llevas las dos advertencias que dan sentido a la lección siguiente. La primera: un snapshot de un disco con una base de datos en marcha es crash-consistent, equivalente al estado tras un corte de luz; PostgreSQL se recupera por WAL, pero tarda y puede perder lo no volcado, y el remedio es coordinar la aplicación con hooks antes y después. La segunda, la más importante del módulo: un snapshot no es una copia de seguridad. Vive en el mismo sistema de almacenamiento que el original, así que no sobrevive a la pérdida de la zona, ni al borrado de la cuenta, ni a una corrupción lógica descubierta tres semanas tarde, ni sirve para restaurar en otro clúster. Es una red de seguridad de corto plazo, excelente para lo que es.
Falta, por tanto, lo que de verdad protege a Rutas Norte: sacar los datos del clúster, ponerlos en un almacén independiente, incluir también los objetos de la API, coordinar la base de datos para que la copia sea consistente, definir retención y —esto es innegociable— probar la restauración. Eso es Copias de Seguridad y Restauración de Datos, la lección que cierra el módulo.
Curso de Kubernetes
Módulo 1: Introducción a Kubernetes
- ¿Qué es Kubernetes?
- Arquitectura de Kubernetes
- Conceptos y Terminología Clave
- Configuración de un Clúster de Kubernetes
- La CLI de Kubernetes: kubectl
- Objetos, Manifiestos YAML y el Modelo Declarativo
- El Proyecto del Curso: la Plataforma Rutas Norte
Módulo 2: Componentes Principales de Kubernetes
- Pods
- ReplicaSets
- Deployments
- Actualizaciones, Rollbacks y Estrategias de Despliegue
- Servicios
- Namespaces
- Etiquetas, Selectores y Anotaciones
Módulo 3: Gestión de Configuración y Secretos
- ConfigMaps
- Secrets
- Variables de Entorno
- Cuotas y Límites de Recursos
- LimitRanges y Clases de Calidad de Servicio (QoS)
- ServiceAccounts y Acceso a la API desde los Pods
Módulo 4: Redes en Kubernetes
- Redes de Clúster
- Tipos de Servicios
- DNS Interno y Descubrimiento de Servicios
- Controladores de Ingress
- TLS y Gestión de Certificados con cert-manager
- Políticas de Red
Módulo 5: Almacenamiento en Kubernetes
- Volúmenes
- Volúmenes Persistentes
- Reclamaciones de Volúmenes Persistentes
- Clases de Almacenamiento
- Aprovisionamiento Dinámico, Expansión y Snapshots
- Copias de Seguridad y Restauración de Datos
Módulo 6: Conceptos Avanzados de Kubernetes
- StatefulSets
- DaemonSets
- Trabajos y CronJobs
- Init Containers, Sidecars y Patrones Multi-Contenedor
- Planificación: Afinidad, Taints y Tolerations
- Definiciones de Recursos Personalizados (CRDs)
- Operadores y el Patrón Controlador
Módulo 7: Monitoreo y Registro
- Verificaciones de Salud y Sondas
- Servidor de Métricas y kubectl top
- Monitoreo con Prometheus
- Visualización y Alertas con Grafana y Alertmanager
- Registro Centralizado con Elasticsearch, Fluentd y Kibana (EFK)
- Depuración de Aplicaciones y Eventos del Clúster
Módulo 8: Seguridad en Kubernetes
- Control de Acceso Basado en Roles (RBAC)
- Contextos de Seguridad y Endurecimiento del Contenedor
- Políticas de Seguridad de Pods y Pod Security Standards
- Seguridad de Red
- Seguridad de Imágenes
- Auditoría, Escaneo y Gestión de Vulnerabilidades
Módulo 9: Escalado y Rendimiento
- Autoescalado Horizontal de Pods
- Autoescalado Vertical de Pods
- Autoescalado de Clúster
- Escalado por Eventos y Métricas Personalizadas con KEDA
- Alta Disponibilidad: PodDisruptionBudgets y Topología
- Ajuste de Rendimiento
Módulo 10: Ecosistema y Herramientas de Kubernetes
- Minikube y Entornos Locales con kind
- Kubeadm
- Helm
- Kustomize
- GitOps con Argo CD y Flux
- Kubernetes Gestionado: EKS, AKS y GKE
Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real
- Despliegue de una Aplicación Web
- Ejecución de Aplicaciones con Estado
- CI/CD con Kubernetes
- Estrategias de Despliegue: Blue-Green y Canary
- Gestión Multi-Clúster
- Operación en Producción: Incidencias, Runbooks y Costes
