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

  1. Qué problema resolvió CSI
  2. La arquitectura: plugin de controlador y plugin de nodo
  3. Los sidecars y los objetos CSIDriver y CSINode
  4. El flujo completo de un volumen, de extremo a extremo
  5. Preparar el clúster de prácticas
  6. Expansión de volúmenes: requisitos y mecánica
  7. Expansión en línea frente a expansión con reinicio
  8. Snapshots: los tres objetos
  9. Crear un snapshot de postgres-reservas
  10. Restaurar desde un snapshot con dataSource
  11. Clonar un PVC
  12. La advertencia: consistencia y qué NO es un snapshot

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

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

  1. Los sidecars y los objetos CSIDriver y CSINode

Aquí 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.

kubectl describe csidriver hostpath.csi.k8s.io
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, Ephemeral

CSINode

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.

kubectl describe csinode rutas-norte
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.

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

  1. 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 volumesnapshotclasses
NAME                     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           1m

Punto 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

  1. 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 -10
Conditions:
  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 succeeded

Y 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/data

20 GiB, con los 8,4 GiB de datos intactos y sin haber parado la base de datos.

  1. 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 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/data

Dos 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).

  1. 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 PV

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

  1. Crear un snapshot de postgres-reservas

El 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 fotografiar
kubectl apply -f k8s/base/volumesnapshotclass.yaml
kubectl apply -f k8s/entornos/dev/snapshot-previo-migracion.yaml
kubectl get volumesnapshot -n rutas-norte-dev
NAME                      READYTOUSE  SOURCEPVC                RESTORESIZE  SNAPSHOTCONTENT           AGE
snap-postgres-previo-v3   true        postgres-reservas-datos  20Gi         snapcontent-a1b2c3d4-...  10s

READYTOUSE: 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.

  1. Restaurar desde un snapshot con dataSource

Ahora 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;"   # -> 1

La 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.io

Al 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-17

Las tres reservas están de vuelta. Cuatro condiciones para que una restauración funcione:

  1. El PVC nuevo debe pedir al menos el restoreSize del snapshot. Menos, y falla.
  2. Debe usar la misma StorageClass (o al menos el mismo driver): un snapshot no cruza sistemas de almacenamiento.
  3. El snapshot debe estar READYTOUSE: true.
  4. El snapshot debe existir en el mismo namespace que el PVC nuevo. Para cruzar namespaces hay que importar el VolumeSnapshotContent a mano.

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

  1. 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
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:

  1. 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.
  2. 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.
  3. Vigila el llenado con antelación con un df -h sobre el punto de montaje, y automatízalo con alertas al 75 % en 07-04.
  4. 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 en kube-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:

  1. Comprueba el tamaño actual del volumen de postgres-reservas desde dentro del contenedor.
  2. Amplíalo de 10 GiB a 20 GiB editando el PVC.
  3. Sigue las condiciones del PVC hasta que termine y verifica el tamaño con df -h.
  4. Intenta reducirlo a 5 GiB y transcribe el error.
  5. Comprueba que las reservas siguen ahí.

Ejercicio 2: snapshot, desastre y restauración

Ensaya el procedimiento completo que usarás en producción:

  1. Inserta cinco reservas ficticias en postgres-reservas.
  2. Crea un VolumeSnapshot llamado snap-practica con las anotaciones de motivo, responsable y ticket. Espera a READYTOUSE: true.
  3. Provoca el desastre: DROP TABLE reservas.
  4. Crea un PVC restaurado con dataSource y apunta el Deployment a él.
  5. Verifica que las cinco reservas han vuelto.
  6. 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/data

El 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 value

La 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"
 count
-------
     5
RTO real: 94 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 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:

  1. 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.
  2. 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.
  3. 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_dump que 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 sidecarsexternal-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

Módulo 2: Componentes Principales de Kubernetes

Módulo 3: Gestión de Configuración y Secretos

Módulo 4: Redes en Kubernetes

Módulo 5: Almacenamiento en Kubernetes

Módulo 6: Conceptos Avanzados de Kubernetes

Módulo 7: Monitoreo y Registro

Módulo 8: Seguridad en Kubernetes

Módulo 9: Escalado y Rendimiento

Módulo 10: Ecosistema y Herramientas de Kubernetes

Módulo 11: Estudios de Caso y Aplicaciones del Mundo Real

Módulo 12: Preparación para la Certificación de Kubernetes

© Copyright 2026. Todos los derechos reservados