Cerramos el módulo con la lección que convierte a Rutas Norte en una plataforma en la que se puede confiar. Ya tenemos los datos fuera del contenedor, en un volumen que sobrevive al pod, expandible y con snapshots. Pero la lección anterior terminó con una advertencia que lo relativiza todo: un snapshot vive en el mismo sistema de almacenamiento que el original, así que no sobrevive a la pérdida de una zona, al borrado de una cuenta ni a una corrupción descubierta tres semanas tarde. Una copia de seguridad de verdad es otra cosa: sale del clúster, va a un sistema independiente, incluye también los objetos de la API, se coordina con la aplicación para ser consistente, tiene retención definida y —esto es lo único innegociable— se prueba restaurándola. En esta lección construirás las dos piezas que Rutas Norte necesita: el volcado lógico programado de postgres-reservas y la copia de la plataforma completa con Velero, y harás el simulacro de desastre borrando un entorno entero para recuperarlo con cronómetro.

Contenido

  1. Qué hay que salvar realmente en un clúster
  2. Snapshot no es copia de seguridad: la regla 3-2-1
  3. RPO y RTO aplicados a Rutas Norte con números
  4. Copia lógica: pg_dump desde un Job programado
  5. Compresión, cifrado y restauración del volcado
  6. Velero: qué hace y cómo está construido
  7. Instalación y primera copia de un namespace
  8. Hooks pre y post: la copia consistente
  9. Copias programadas con retención
  10. Restaurar en otro namespace
  11. El simulacro de desastre
  12. Qué no se restaura solo y el runbook mínimo
  13. Retención, coste y datos personales

  1. Qué hay que salvar realmente en un clúster

La primera pregunta no es "cómo hago copias" sino "de qué". En un clúster de Kubernetes hay tres cosas distintas, y cada una necesita una técnica diferente:

Qué Dónde vive Técnica ¿Ya lo tenemos?
Los manifiestos (Deployments, Services, Ingress, NetworkPolicies…) En Git, como fuente de verdad Control de versiones : k8s/base y k8s/entornos/...
El estado de la API (objetos reales, incluidos los creados fuera de Git: Secrets, PVC, certificados de cert-manager, anotaciones puestas por controladores) En etcd Copia de objetos de la API (Velero) o copia de etcd No
Los datos de los volúmenes En los PV Volcado lógico y/o snapshot exportado No

Tres observaciones que orientan todo lo demás. Git no basta: los manifiestos permiten recrear la forma de la plataforma, pero no contienen los Secrets (que dejamos deliberadamente fuera de Git en 03-02), ni los certificados emitidos por cert-manager (04-05), ni por supuesto los datos. La copia de etcd es cosa del administrador del clúster, no del equipo de aplicación, y en un clúster gestionado ni siquiera tienes acceso a etcd: por eso el enfoque práctico para Rutas Norte es copiar objetos de la API por namespace, que además es portable entre clústeres. Y lo verdaderamente irreemplazable son los datos: un Deployment se recrea en segundos; una reserva vendida y cobrada, no.

  1. Snapshot no es copia de seguridad: la regla 3-2-1

Retomamos la advertencia de 05-05 y le damos forma operativa con la regla clásica, que sigue siendo el mejor resumen. 3-2-1: al menos 3 copias de los datos, en 2 soportes o sistemas distintos, con 1 de ellas fuera del emplazamiento principal. Aplicada a Rutas Norte:

Elemento Cuenta como Cumple
El volumen de postgres-reservas en producción La copia 1 (el original)
Snapshots CSI diarios y previos a cambios La copia 2, mismo sistema 3 parcial, 2 no, 1 no
Volcados pg_dump en un PVC del clúster La copia 3, mismo clúster 3 sí, 2 parcial, 1 no
Copias Velero en un almacén de objetos en otra región 1 sí

La cuarta fila es la que convierte el conjunto en una estrategia: sin ella, todo lo demás desaparece junto con la infraestructura que protege. Dos refuerzos que hoy se consideran obligatorios: la inmutabilidad —el almacén de objetos en modo bloqueo (Object Lock / WORM), para que ni siquiera unas credenciales robadas puedan borrar o cifrar las copias— y la separación de credenciales: la cuenta que escribe las copias no debe poder borrarlas, y el destino debe estar en otra cuenta o suscripción.

  1. RPO y RTO aplicados a Rutas Norte con números

Dos siglas que hay que separar bien porque se confunden constantemente:

RPO (Recovery Point Objective) RTO (Recovery Time Objective)
Pregunta ¿Cuántos datos podemos perder? ¿Cuánto tiempo podemos estar caídos?
Lo determina La frecuencia de las copias La velocidad de la restauración

Y ahora los números, que es lo que convierte la conversación en algo útil. Rutas Norte vende unas 60 reservas por hora en día laborable normal, y unas 400 en puente o inicio de vacaciones, con picos de 700.

Frecuencia de copia (RPO) Perdidas en día normal Perdidas en puente Coste operativo
24 h (copia nocturna) hasta 1.440 hasta 9.600 Mínimo
6 h hasta 360 hasta 2.400 Bajo
1 h hasta 60 hasta 400 Medio
5 min (WAL archivado continuo) ~5 ~33 Alto

Cada fila implica una conversación de negocio, no técnica: ¿cuánto cuesta perder 9.600 reservas? No es solo el importe; es reconstruir a mano, atender reclamaciones, clientes que se presentan en la estación sin billete y daño reputacional en plena temporada alta.

Decisión de Rutas Norte:

Entorno RPO objetivo RTO objetivo Cómo se consigue
rutas-norte-pro 1 hora (15 min en temporada alta) 2 horas Volcado lógico horario + Velero con snapshot + WAL archivado (pendiente)
rutas-norte-pre 24 h 8 h Velero nocturno
rutas-norte-dev Sin objetivo Mejor esfuerzo Se recrea desde Git

El RTO medido en el ejercicio de 05-05 —del orden de dos minutos en local— era el de restaurar un volumen. El RTO real de un desastre incluye detectar, decidir, restaurar, verificar y reabrir el servicio. Por eso hay que medirlo con un simulacro (apartado 11), no estimarlo.

  1. Copia lógica: pg_dump desde un Job programado

La copia lógica es un fichero con las instrucciones necesarias para reconstruir la base de datos. Sus ventajas frente a un snapshot de bloques son decisivas:

Volcado lógico (pg_dump) Snapshot de bloques
Consistencia Garantizada: se ejecuta en una transacción Crash-consistent salvo coordinación
Verificable Sí: si se restaura, los datos son coherentes No demuestra nada por sí mismo
Portable a otra versión, otro clúster, otro proveedor No
Permite restaurar una sola tabla No
Tamaño Pequeño (comprime muy bien) El del volumen
Velocidad en bases grandes Lenta Instantánea

No compiten: se complementan. El snapshot es rápido para deshacer; el volcado es la copia que de verdad demuestra que los datos están bien.

Primero, el volumen donde se escriben los volcados: k8s/base/copias-postgres-pvc.yaml, un PVC llamado copias-postgres de 50 GiB, ReadWriteOnce, con las etiquetas habituales más app.kubernetes.io/component: copias-de-seguridad. Va en la clase rutasnorte-rapida (con Retain) porque si se pierden las copias, la red de seguridad desaparece.

Y ahora el CronJob. Nota: el objeto CronJob se estudia a fondo en 06-03; aquí lo usamos como herramienta, quedándonos con lo imprescindible para leerlo.

# k8s/entornos/pro/cronjob-copia-postgres.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: copia-postgres-reservas
  namespace: rutas-norte-pro
  labels:
    app: copia-postgres-reservas
    app.kubernetes.io/component: copias-de-seguridad
    app.kubernetes.io/part-of: rutas-norte
    entorno: pro
spec:
  schedule: "0 * * * *"              # cada hora en punto (RPO de 1 h)
  timeZone: "Europe/Madrid"
  concurrencyPolicy: Forbid          # nunca dos volcados a la vez
  successfulJobsHistoryLimit: 3      # y failedJobsHistoryLimit: 5
  jobTemplate:
    spec:
      backoffLimit: 2
      activeDeadlineSeconds: 3600    # si tarda mas de 1 h, algo va mal
      template:
        metadata:
          labels: { app: copia-postgres-reservas, entorno: pro }
        spec:
          restartPolicy: Never
          serviceAccountName: copias-postgres    # SA propia (03-06)
          securityContext: { runAsNonRoot: true, runAsUser: 999, fsGroup: 999 }
          containers:
            - name: pg-dump
              image: postgres:16.4
              env:
                - { name: PGHOST, value: postgres-reservas }   # el Service (02-05)
                - name: PGUSER
                  valueFrom:
                    secretKeyRef: { name: postgres-reservas-credenciales, key: username }
                - name: PGPASSWORD
                  valueFrom:
                    secretKeyRef: { name: postgres-reservas-credenciales, key: password }
                - name: PGDATABASE
                  valueFrom:
                    secretKeyRef: { name: postgres-reservas-credenciales, key: database }
              command:
                - /bin/bash
                - -c
                - |
                  set -euo pipefail
                  DESTINO="/copias/reservas-$(date +%Y%m%d-%H%M%S).dump"
                  # -Fc: formato comprimido de PostgreSQL; permite restaurar
                  #      tablas sueltas. -Z6: nivel de compresion (0-9).
                  pg_dump -Fc -Z6 --no-owner --no-privileges -f "${DESTINO}"
                  # Verificacion basica: el volcado se puede LEER.
                  pg_restore --list "${DESTINO}" > /dev/null
                  echo "[$(date -Is)] volcado correcto: $(du -h ${DESTINO} | cut -f1)"
                  # Retencion local: 72 h en el PVC. La larga va al almacen
                  # de objetos con Velero (apartado 9).
                  find /copias -name 'reservas-*.dump' -mmin +4320 -print -delete
              volumeMounts:
                - { name: copias, mountPath: /copias }
              resources:
                requests: { cpu: 200m, memory: 256Mi }
                limits:   { cpu: "1",  memory: 1Gi }
          volumes:
            - name: copias
              persistentVolumeClaim: { claimName: copias-postgres }

Las decisiones que hay dentro, una a una:

  • concurrencyPolicy: Forbid: si un volcado tarda más de una hora, el siguiente no arranca. Dos pg_dump simultáneos duplicarían la carga sobre la base de datos justo cuando ya va lenta.
  • set -euo pipefail y la verificación con pg_restore --list: sin lo primero, un pg_dump fallido puede dejar un fichero truncado y el Job terminar en Completed, dándote copias que no valen; lo segundo comprueba que el volcado es al menos legible. La verificación de verdad es restaurarlo (apartado 11).
  • --no-owner --no-privileges: el volcado no arrastra propietarios ni permisos, lo que permite restaurarlo en otro clúster con otro usuario. Y con la ServiceAccount propia (03-06) el pod solo tiene lo que necesita, mientras que la retención local de 72 h convierte el PVC en la copia caliente para restaurar rápido; la retención larga vive fuera del clúster.

Y hay que recordar las NetworkPolicies de 04-06: el pod del CronJob es nuevo en el namespace y deny-all lo bloqueará. Hay que autorizar explícitamente su conversación:

# k8s/entornos/pro/np-06-copias-postgres.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: copias-postgres-egress
  namespace: rutas-norte-pro
spec:
  podSelector:
    matchLabels: { app: copia-postgres-reservas }
  policyTypes: [Egress]
  egress:
    - to:
        - podSelector: { matchLabels: { app: postgres-reservas } }
      ports: [{ protocol: TCP, port: 5432 }]
    - to:                                    # DNS, o nada resuelve
        - namespaceSelector:
            matchLabels: { kubernetes.io/metadata.name: kube-system }
          podSelector: { matchLabels: { k8s-app: kube-dns } }
      ports:
        - { protocol: UDP, port: 53 }
        - { protocol: TCP, port: 53 }

Verificación:

kubectl apply -f k8s/base/copias-postgres-pvc.yaml
kubectl apply -f k8s/entornos/pro/cronjob-copia-postgres.yaml
kubectl apply -f k8s/entornos/pro/np-06-copias-postgres.yaml
# Lanzar una ejecucion manual sin esperar a la hora en punto
kubectl create job --from=cronjob/copia-postgres-reservas \
  copia-manual-$(date +%s) -n rutas-norte-pro
kubectl logs -n rutas-norte-pro -l app=copia-postgres-reservas --tail=20
# [2026-08-05T12:00:41+02:00] volcado correcto: 412M

  1. Compresión, cifrado y restauración del volcado

Cifrado

El formato -Fc comprime pero no cifra. Y ese fichero contiene el nombre, el DNI, el teléfono y el correo de todos los clientes de Rutas Norte: es exactamente el tipo de artefacto que no puede quedar en claro en ningún soporte. La solución práctica es cifrar con una clave pública cuya clave privada no está en el clúster:

# En el contenedor del CronJob, sustituyendo el pg_dump -f por una tuberia:
pg_dump -Fc -Z6 --no-owner --no-privileges \
  | age -r "$CLAVE_PUBLICA_COPIAS" -o "${DESTINO}.age"

La variable CLAVE_PUBLICA_COPIAS llega por secretKeyRef desde un Secret copias-clave-publica. Y ahí está el punto importante: en el clúster solo vive la clave pública, así que quien comprometa el clúster puede escribir copias pero no puede leerlas. La clave privada se custodia fuera (gestor de secretos corporativo, caja fuerte), y su procedimiento de acceso debe estar documentado, porque una copia que nadie puede descifrar el día del desastre no es una copia.

Restauración

Se lanza un pod auxiliar con la imagen postgres:16.4 que monte el PVC copias-postgres en /copias (con kubectl run ... --overrides o un Job de un solo uso), y desde dentro:

# 1. Localizar el volcado
ls -lh /copias/

# 2. Restaurar sobre una base de datos NUEVA (nunca sobre la de produccion)
export PGHOST=postgres-reservas PGUSER=rutasnorte PGPASSWORD=...
createdb reservas_restaurada
pg_restore -d reservas_restaurada --no-owner --jobs=4 \
  /copias/reservas-20260805-120003.dump

# 3. Verificar ANTES de tocar nada real
psql -d reservas_restaurada -c "SELECT count(*), max(fecha) FROM reservas;"
#  count  |    max
# --------+------------
#  184392 | 2026-12-28

Restaura siempre a una base de datos nueva y verifica antes de sustituir. Restaurar directamente sobre la base de producción con --clean es la forma más rápida de convertir un incidente recuperable en uno irreversible.

Opciones de pg_restore que ahorran tiempo el día malo:

--jobs=4 restaura en paralelo y reduce mucho el RTO en bases grandes; --table=reservas restaura una sola tabla, que es el caso más común (alguien borró una tabla); --schema-only y --data-only separan estructura y datos; y --list con --use-list permiten inspeccionar el contenido y restaurar solo una parte.

  1. Velero: qué hace y cómo está construido

El volcado lógico protege los datos de la base. No protege los Secrets, ni los PVC, ni los Ingress, ni los certificados, ni el resto de objetos del namespace. Para eso está Velero, la herramienta estándar de copia y migración de recursos de Kubernetes.

Velero hace tres cosas. Copia los objetos de la API de un namespace (o del clúster entero), filtrados por etiquetas o por tipo, a un almacén de objetos (S3, Azure Blob, GCS, MinIO). Copia los datos de los volúmenes por dos vías: pidiendo snapshots al driver CSI, o copiando fichero a fichero con Kopia/Restic al mismo almacén de objetos, que es lo que de verdad saca los datos del sistema de almacenamiento. Y restaura todo eso, en el mismo clúster o en otro, con reasignación de namespaces y filtros.

flowchart TB
    subgraph CLUSTER["Cluster de Kubernetes"]
        API["kube-apiserver"]
        SRV["Deployment velero (controlador)<br/>+ plugins de objetos y CSI"]
        NA["DaemonSet node-agent<br/>(copia de ficheros con Kopia)"]
        PVCS[("PVC de rutas-norte-pro")]
    end
    OBJ[("Almacen de objetos<br/>s3://rutasnorte-copias<br/>OTRA REGION, inmutable")]
    SNAP[("Snapshots CSI<br/>mismo sistema de almacenamiento")]

    SRV -->|"lee objetos"| API
    SRV -->|"manifiestos + metadatos"| OBJ
    SRV -->|"VolumeSnapshot"| SNAP
    NA -->|"datos fichero a fichero"| OBJ
    PVCS --- NA

La distinción que hay que tener clara desde el principio:

Método de volumen Dónde acaban los datos ¿Sobrevive a perder la región? Velocidad
Snapshots CSI (--snapshot-volumes) En el mismo sistema de almacenamiento No Muy rápida
Ficheros (--default-volumes-to-fs-backup) En el almacén de objetos Lenta

Para Rutas Norte: snapshots CSI para lo diario (rápido, para deshacer) y copia al almacén de objetos para la copia semanal de larga retención, que es la que cumple el "1" de la regla 3-2-1.

  1. Instalación y primera copia de un namespace

Para practicar en minikube usaremos MinIO como almacén de objetos compatible con S3, desplegado en el propio clúster. En producción sería un bucket real en otra región y otra cuenta.

# 1. CLI de Velero (descarga la release y mueve el binario a /usr/local/bin)
velero version --client-only
# 2. Credenciales del almacen (en produccion, de una cuenta que NO pueda borrar)
printf '[default]\naws_access_key_id=minio\naws_secret_access_key=minio123\n' \
  > credenciales-velero
# 3. Instalar el servidor en el cluster
velero install --provider aws \
  --plugins velero/velero-plugin-for-aws:v1.11.0,velero/velero-plugin-for-csi:v0.7.0 \
  --bucket rutasnorte-copias --secret-file ./credenciales-velero \
  --use-node-agent --features=EnableCSI \
  --backup-location-config region=minio,s3ForcePathStyle="true",s3Url=http://minio.velero.svc:9000 \
  --snapshot-location-config region=minio

kubectl get pods -n velero
velero backup-location get      # PHASE debe decir Available

La primera copia, del namespace de preproducción:

velero backup create pre-completa-$(date +%Y%m%d) \
  --include-namespaces rutas-norte-pre --default-volumes-to-fs-backup \
  --ttl 168h0m0s --labels entorno=pre,tipo=manual
velero backup describe pre-completa-20260805 --details
Name: pre-completa-20260805    Phase: Completed    TTL: 168h0m0s
Resource List:
  apps/v1/Deployment: 5      v1/Service: 5         v1/ConfigMap: 7
  v1/Secret: 4               v1/ServiceAccount: 6  v1/PersistentVolumeClaim: 2
  networking.k8s.io/v1/Ingress: 2    networking.k8s.io/v1/NetworkPolicy: 6
Backup Volumes (Pod Volume Backups - kopia):
  postgres-reservas-.../datos: Completed (18.4GB)

Fíjate en lo que ha capturado: los Secrets, que no están en Git; los PVC; las NetworkPolicies; los ServiceAccounts. Y los datos del volumen, 18,4 GB copiados fichero a fichero al almacén de objetos.

Las dos variantes de volúmenes son --snapshot-volumes=false (solo objetos de la API: rapidísimo, pero sin datos) y --snapshot-volumes=true (snapshots CSI: rápido, pero los datos se quedan en el mismo sistema de almacenamiento).

  1. Hooks pre y post: la copia consistente

Aquí resolvemos el problema de consistencia que dejamos abierto en 05-05. Los hooks de Velero ejecutan un comando dentro del contenedor antes y después de copiar el volumen, lo que permite dejar la base de datos en un estado coherente.

Se declaran como anotaciones en el pod, o en el propio backup. La forma declarativa, en el podTemplate del Deployment:

    metadata:
      labels: { app: postgres-reservas, entorno: pro }
      annotations:
        # ANTES de copiar el volumen: modo copia de PostgreSQL
        pre.hook.backup.velero.io/container: postgres
        pre.hook.backup.velero.io/command: >-
          ["/bin/bash","-c",
           "psql -U rutasnorte -d reservas -c \"SELECT pg_backup_start('velero', true);\" &&
            psql -U rutasnorte -d reservas -c 'CHECKPOINT;'"]
        pre.hook.backup.velero.io/timeout: 3m
        # DESPUES: salir del modo copia SIEMPRE, haya ido bien o mal
        post.hook.backup.velero.io/container: postgres
        post.hook.backup.velero.io/command: >-
          ["/bin/bash","-c",
           "psql -U rutasnorte -d reservas -c 'SELECT pg_backup_stop();'"]
        post.hook.backup.velero.io/timeout: 3m

Qué hace cada uno. pg_backup_start (nombre a partir de PostgreSQL 15; antes era pg_start_backup) pone el servidor en modo copia: fuerza un checkpoint y garantiza que los ficheros del directorio de datos formen un conjunto restaurable, aunque sigan llegando escrituras. Y pg_backup_stop cierra ese modo: es imprescindible que se ejecute siempre, porque una base de datos que se queda en modo copia acumula WAL sin liberar y acaba llenando el disco; por eso el hook post se ejecuta aunque la copia falle.

Alternativa más simple y a menudo preferible para bases medianas: el hook pre hace directamente un pg_dump a un emptyDir que Velero también copia. Sacrifica algo de tiempo a cambio de una copia lógica verificable.

Nivel Cómo Consistencia Coste
Sin hooks Copia directa del volumen Crash-consistent Nulo
Hooks pg_backup_start/stop Modo copia de PostgreSQL Consistente a nivel de aplicación Bajo
Hook con pg_dump Volcado lógico dentro de la copia Consistente y verificable Medio
velero backup create pro-consistente-$(date +%Y%m%d) \
  --include-namespaces rutas-norte-pro --default-volumes-to-fs-backup
velero backup logs pro-consistente-20260805 | grep -i hook
# level=info msg="Running exec hook" hookPhase=pre  pod=.../postgres-reservas-...
# level=info msg="Running exec hook" hookPhase=post pod=.../postgres-reservas-...

  1. Copias programadas con retención

Una copia manual sirve para aprender; lo que protege es una programación con retención automática, que Velero resuelve con Schedule y el TTL, que borra la copia y sus datos al expirar.

# Diaria de produccion con snapshots: rapida, retencion corta (7 dias)
velero schedule create pro-diaria --schedule="0 2 * * *" \
  --include-namespaces rutas-norte-pro --snapshot-volumes=true \
  --ttl 168h0m0s --labels entorno=pro,tipo=diaria

# Semanal al almacen de objetos: lenta, retencion larga (90 dias)
velero schedule create pro-semanal --schedule="0 3 * * 0" \
  --include-namespaces rutas-norte-pro --default-volumes-to-fs-backup \
  --ttl 2160h0m0s --labels entorno=pro,tipo=semanal

# Preproduccion (14 dias)
velero schedule create pre-diaria --schedule="0 4 * * *" \
  --include-namespaces rutas-norte-pre --default-volumes-to-fs-backup \
  --ttl 336h0m0s
velero schedule get      # las tres, en estado Enabled

El calendario resultante de Rutas Norte:

Copia Frecuencia Método Retención Protege de
Volcado pg_dump (CronJob) Cada hora Lógico, a PVC 72 h Error humano en los datos, reciente
Velero pro-diaria Diaria Snapshots CSI 7 días Fallo de despliegue, borrado de objetos
Velero pro-semanal Semanal Ficheros al almacén 90 días Pérdida de zona, cuenta, corrupción antigua
Snapshot manual Antes de cada cambio con riesgo CSI 72 h Migración fallida

Solo la fila pro-semanal cumple el "1" de la regla 3-2-1. Las demás son comodidad y velocidad.

Vigilancia obligatoria: una copia que falla en silencio es peor que no tener copias, porque genera confianza infundada. Se detecta con velero backup get | grep -v Completed y se investiga con velero backup describe <nombre> --details y velero backup logs <nombre> | grep -i error. Esa comprobación debe ser una alerta automática, no una revisión manual; se monta con las herramientas de 07-04.

  1. Restaurar en otro namespace

Restaurar sobre el namespace original en producción es la operación más delicada. La práctica correcta es restaurar primero en otro sitio y verificar:

velero restore create verificacion-$(date +%s) \
  --from-backup pro-semanal-20260802030012 \
  --namespace-mappings rutas-norte-pro:rutas-norte-verificacion \
  --include-resources deployments,services,configmaps,secrets,persistentvolumeclaims \
  --wait
velero restore describe verificacion-1754392011 --details
kubectl get all,pvc -n rutas-norte-verificacion
Phase:  Completed
Warnings:
  rutas-norte-verificacion:  could not restore, Ingress "tienda-web" already exists

Opciones de restauración que se usan a diario:

Opción Para qué
--namespace-mappings origen:destino Restaurar en otro namespace: verificación, o clonar pre a partir de pro
--include-resources / --exclude-resources / --selector Restaurar solo lo necesario: ciertos tipos, o un solo componente
--existing-resource-policy=update Sobrescribir lo que ya existe (por defecto no lo hace)
--restore-volumes=false Solo los objetos, sin datos

Y el aviso que ahorra un susto: por defecto Velero NO sobrescribe los objetos que ya existen. Si restauras sobre un namespace vivo, verás un montón de avisos "already exists" y creerás que la restauración falló, cuando lo que ha hecho es protegerte. Para una recuperación real, el namespace destino debe estar vacío o hay que usar --existing-resource-policy=update conscientemente.

  1. El simulacro de desastre

Una copia no probada no es una copia. Esta es la práctica obligatoria del módulo, y en Rutas Norte se ejecuta cada trimestre sobre rutas-norte-pre, con cronómetro y con acta.

# --- ESTADO PREVIO: documentar lo que debe volver ---
kubectl get all,pvc,secret,ingress,networkpolicy -n rutas-norte-pre --no-headers | wc -l
kubectl exec -n rutas-norte-pre deploy/postgres-reservas -- \
  psql -U rutasnorte -d reservas -c "SELECT count(*) FROM reservas;" | tee inventario-previo.txt
curl -s -o /dev/null -w "%{http_code}\n" https://pre.rutasnorte.example/

# --- COPIA DE PARTIDA ---
velero backup create simulacro-$(date +%Y%m%d) \
  --include-namespaces rutas-norte-pre --default-volumes-to-fs-backup --wait

# --- EL DESASTRE ---
T0=$(date +%s); kubectl delete namespace rutas-norte-pre

# --- RECUPERACION ---
velero restore create recuperacion-$(date +%s) \
  --from-backup simulacro-20260805 --wait
kubectl wait --for=condition=available --timeout=900s deployment --all -n rutas-norte-pre

# --- VERIFICACION: no basta con que los pods esten Running ---
kubectl get all,pvc -n rutas-norte-pre
kubectl exec -n rutas-norte-pre deploy/postgres-reservas -- \
  psql -U rutasnorte -d reservas -c "SELECT count(*) FROM reservas;"   # -> 184392
curl -s -o /dev/null -w "%{http_code}\n" https://pre.rutasnorte.example/   # -> 200
echo "RTO MEDIDO: $(( ($(date +%s) - T0) / 60 )) minutos"   # -> 23 minutos

El acta del simulacro debe recoger, como mínimo: fecha y responsable, copia utilizada, RTO medido, RPO efectivo (el tiempo transcurrido entre la copia y el desastre), qué no volvió solo, incidencias y acciones derivadas. En el segundo ejercicio la redactarás entera.

El apartado "qué no volvió solo" es el más valioso del acta. Es literalmente imposible de conocer sin hacer el simulacro, y es lo que convierte una recuperación teórica en una recuperación real.

  1. Qué no se restaura solo y el runbook mínimo

Elemento Por qué no vuelve Qué hay que hacer
Certificados TLS cert-manager los reemite; si se restaura el Secret antiguo puede estar caducado Dejar que se reemitan; vigilar los límites de tasa de Let's Encrypt (04-05)
IP del LoadBalancer y registros DNS Se asignan IP nuevas al recrear el Service, y el DNS vive fuera del clúster Reservar IP estáticas y actualizar www y api de rutasnorte.example, contando con la propagación
Secretos externos Tokens de la pasarela de pagos, del proveedor SMTP: pueden haber rotado Regenerar desde el gestor de secretos corporativo
Cortafuegos, grupos de seguridad y objetos de otros namespaces Los primeros son de la nube, no del clúster; los segundos no entran si la copia era de un solo namespace Infraestructura como código, y copiar también ingress-nginx, cert-manager y velero
Recursos de clúster (StorageClasses, ClusterIssuers, ClusterRoles) No entran en una copia de namespace, y sin ellos los PV no se aprovisionan --include-cluster-resources=true, y crear las StorageClasses antes de restaurar

Runbook mínimo de recuperación de Rutas Norte

Un documento corto, en el repositorio, que alguien de guardia a las tres de la mañana pueda seguir sin pensar:

RUNBOOK: recuperacion de rutas-norte-pro
Responsable de guardia: [email protected]   |   Ultima prueba: 2026-08-05

0. DECLARAR EL INCIDENTE. Abrir canal, anotar hora T0, avisar a atencion al
   cliente. NO improvisar: si hay dudas sobre el alcance, ir al paso 1 igual.
1. EVALUAR el alcance
   kubectl get nodes; kubectl get all -n rutas-norte-pro; velero backup get
   -> Solo datos corruptos?  -> paso 3   |   Namespace destruido? -> paso 4
   -> Cluster perdido?       -> paso 5
2. CONTENER. Escalar a 0 lo que pueda seguir escribiendo datos malos:
   kubectl scale deploy api-reservas worker-notificaciones -n rutas-norte-pro --replicas=0
3. RESTAURACION LOGICA (RTO ~30 min)
   Volcado mas reciente en el PVC copias-postgres -> pg_restore a reservas_restaurada
   VERIFICAR recuento y fecha maxima -> renombrar bases de datos -> arrancar
4. RESTAURACION DEL NAMESPACE (RTO ~45 min)
   velero restore create --from-backup <ultima Completed>
   Comprobar: pods Ready, PVC Bound, Ingress con IP, certificado valido
   Actualizar DNS si la IP del LoadBalancer ha cambiado
5. CLUSTER NUEVO (RTO ~4 h)
   Crear cluster -> addons (ingress-nginx, cert-manager, csi, velero)
   Crear StorageClasses ANTES de restaurar
   velero restore create --include-cluster-resources=true
   Regenerar secretos externos (pagos, SMTP) y actualizar DNS
6. VERIFICAR EL NEGOCIO, no solo los pods: recuento y fecha maxima de
   reservas, compra de un billete de prueba de extremo a extremo, y
   https://www.rutasnorte.example y https://api.rutasnorte.example -> 200
7. CERRAR: anotar T_fin, RTO y RPO reales, y abrir el post-mortem.

  1. Retención, coste y datos personales

Coste

Las copias cuestan dinero, y sin retención automática el gasto crece sin freno. Con postgres-reservas en unos 20 GiB, los 72 volcados horarios ocupan unos 30 GiB (comprimen a ~410 MB cada uno), los 7 snapshots diarios unos 25 GiB por ser incrementales, y las 13 copias semanales unos 260 GiB en el almacén de objetos. Palancas para ajustarlo: usar clases de almacenamiento frías para lo antiguo, aprovechar la deduplicación de Kopia, y no copiar lo que se puede regenerar (redis-cache no entra en ninguna copia, por lo decidido en 05-03).

Datos personales

Y aquí está lo que no puede quedar como detalle técnico. Las copias de seguridad de Rutas Norte contienen datos personales de clientes: nombre, DNI, teléfono y correo de cada persona que ha comprado un billete. Eso significa que cada copia es un tratamiento de datos personales con los mismos deberes que el sistema original, y en algunos aspectos más exigentes, porque las copias se multiplican, se replican y se olvidan.

Los requisitos mínimos que Rutas Norte aplica:

Requisito Cómo se implementa aquí
Cifrado en reposo y en tránsito Volcados cifrados con age (clave privada fuera del clúster); almacén de objetos con cifrado del lado del servidor y acceso por HTTPS; volúmenes con encrypted: "true" en la StorageClass (05-04); tráfico interno segmentado por NetworkPolicies (04-06)
Control de acceso ServiceAccount dedicada y RBAC mínimo (08-01); credenciales del almacén con permisos de solo escritura
Plazo de borrado definido TTL en cada Schedule de Velero; find -mmin +4320 -delete en el CronJob. Sin retención automática, las copias son eternas y eso es incumplimiento
Ámbito geográfico El bucket de copias debe estar en una región prevista y declarada. Copiar a otra región para resistir desastres no puede sacar los datos del ámbito legal permitido
Registro de accesos y derecho de supresión Auditoría de quién descarga o restaura una copia (08-06); y si un cliente ejerce su derecho de borrado, hay que decidir y documentar qué ocurre con las copias que lo contienen
Entornos no productivos Un clon de producción en pre o en un portátil de desarrollo son datos reales en un entorno menos protegido. Anonimizar o pseudonimizar

ADVERTENCIA DE CUMPLIMIENTO NORMATIVO

Todo lo anterior es arquitectura técnica, no asesoramiento legal. Los plazos de retención, la ubicación geográfica de las copias, el tratamiento del derecho de supresión sobre datos ya copiados, la base legal del tratamiento y las obligaciones ante una brecha de seguridad deben ser revisados y aprobados por el responsable de cumplimiento normativo y de protección de datos de la organización antes de poner en marcha esta configuración en producción. Los valores de este curso (72 horas, 7 días, 90 días) son ejemplos didácticos elegidos para ilustrar la mecánica de la retención, no recomendaciones legales: el plazo correcto depende de la normativa aplicable, del sector y de la finalidad declarada de cada tratamiento, y conservar datos personales más tiempo del necesario es tan incumplimiento como no protegerlos.

Errores Comunes y Consejos

Error Síntoma Solución
Confiar en snapshots como única copia Pérdida total si cae la zona o la cuenta Copia en almacén de objetos, otra región, otra cuenta
No probar nunca la restauración Se descubre el día del desastre que la copia no vale Simulacro trimestral con acta
Restaurar directamente sobre producción Un incidente recuperable se vuelve irreversible Restaurar a base o namespace nuevos y verificar
Copias que fallan en silencio, o sin set -euo pipefail en el volcado Ficheros truncados, Jobs en Completed y confianza infundada durante meses Fallar ruidosamente, verificar el volcado y alertar sobre estados distintos de Completed
Volcados sin cifrar, o copiar producción a desarrollo sin anonimizar Datos personales en claro en un PVC, un bucket o un entorno poco protegido Cifrado con clave pública (privada fuera del clúster); anonimizar o pseudonimizar
Sin retención Coste creciente e incumplimiento normativo TTL en los Schedules y limpieza en el CronJob
Olvidar los recursos de clúster La restauración falla por StorageClasses inexistentes --include-cluster-resources=true; crear clases antes
Olvidar las NetworkPolicies con el CronJob El volcado no conecta con la base de datos Autorizar su egress explícitamente (04-06)
Hook pre sin hook post La base se queda en modo copia y llena el disco de WAL El post debe ejecutarse siempre
Creer que el RTO es el tiempo de velero restore El simulacro real da el triple Medir de extremo a extremo, incluidos DNS y verificación

Consejos:

  1. Automatiza la verificación, no solo la copia. Un Job semanal que restaure el último volcado en un namespace de pruebas y compare el recuento de filas convierte "creemos que tenemos copias" en "sabemos que tenemos copias".
  2. Documenta el RPO y el RTO reales, no los deseados, y revísalos tras cada simulacro. Son el dato que necesita el negocio para decidir cuánto invertir.
  3. La copia debe poder restaurarse sin la persona que la configuró. Si el procedimiento vive en la cabeza de alguien, no existe: escríbelo en el runbook, en el repositorio. Y cuando dudes entre gastar en copias o en cualquier otra cosa, gasta en copias: es el único componente de la plataforma cuya ausencia no se nota hasta que es demasiado tarde.

Ejercicios

Ejercicio 1: el volcado programado

En rutas-norte-dev, monta la cadena completa de copia lógica:

  1. Crea el PVC copias-postgres de 5 GiB en la clase rutasnorte-rapida.
  2. Adapta el CronJob del apartado 4 a dev, con schedule: "*/10 * * * *" para no esperar una hora.
  3. Lanza una ejecución manual y comprueba en los registros que el volcado se ha creado y verificado.
  4. Borra la tabla reservas y restáurala a una base de datos nueva desde el volcado, sin tocar la original.
  5. Comprueba que los datos coinciden y explica por qué es importante restaurar a una base nueva.

Ejercicio 2: simulacro de desastre en rutas-norte-pre

Ejecuta el simulacro completo del apartado 11 sobre rutas-norte-pre (créalo con los cinco componentes si no lo tienes) y redacta el acta con: inventario previo, copia usada, RTO medido, RPO efectivo, qué no volvió solo, incidencias y acciones derivadas.

Compara después el RTO medido con el objetivo de 8 horas fijado para pre en el apartado 3 y razona si la política es adecuada o hay que cambiarla.

Ejercicio 3: diseñar la estrategia de rutas-norte-pro

El comité de dirección de Rutas Norte pregunta: "si mañana perdemos el centro de datos entero, ¿cuánto tardamos en volver a vender y cuántas reservas perdemos?". Prepara la respuesta en forma de documento breve:

  • A. Tabla de qué se copia, con qué método, frecuencia, retención y de qué protege cada copia.
  • B. RPO y RTO comprometidos, con el número de reservas perdidas en el peor caso (puente, 400 reservas/hora).
  • C. Los tres elementos que no se restauran solos y quién es responsable de cada uno.
  • D. Un apartado sobre datos personales que incluya qué contienen las copias, cómo se protegen y qué debe validar el responsable de cumplimiento normativo.

Soluciones

Ejercicio 1

# 1, 2 y 3. El PVC y el CronJob del apartado 4, con namespace rutas-norte-dev,
#           5Gi y schedule "*/10 * * * *". Despues, ejecucion manual:
kubectl apply -f k8s/entornos/dev/copias-postgres-pvc.yaml
kubectl create job --from=cronjob/copia-postgres-reservas \
  copia-prueba -n rutas-norte-dev
kubectl wait --for=condition=complete job/copia-prueba -n rutas-norte-dev --timeout=300s
kubectl logs job/copia-prueba -n rutas-norte-dev
# [2026-08-05T13:10:04+02:00] volcado correcto: 12K

# 4: el desastre y la restauracion, desde un pod con el PVC de copias montado
kubectl exec -n rutas-norte-dev deploy/postgres-reservas -- \
  psql -U rutasnorte -d reservas -c "DROP TABLE reservas;"

# Dentro de ese pod auxiliar:
createdb reservas_restaurada
pg_restore -d reservas_restaurada --no-owner /copias/reservas-20260805-131002.dump
psql -d reservas_restaurada -c "SELECT count(*) FROM reservas;"    # -> 5

# 5: promover la restauracion, ya verificada
psql -d postgres -c "ALTER DATABASE reservas RENAME TO reservas_danyada;"
psql -d postgres -c "ALTER DATABASE reservas_restaurada RENAME TO reservas;"

Por qué a una base nueva. Tres razones. Primera, si el volcado estuviera corrupto o incompleto, restaurar encima con --clean habría destruido lo que quedaba: te quedarías sin lo dañado y sin la copia. Segunda, la base dañada es la evidencia para el post-mortem: sin ella no sabrás qué pasó ni desde cuándo. Y tercera, permite comparar antes de cortar: recuento de filas, fecha máxima, integridad referencial. La sustitución final es un renombrado de dos segundos, así que el ahorro de tiempo de restaurar encima es despreciable frente al riesgo.

Ejercicio 2

velero backup create simulacro-pre-$(date +%Y%m%d) \
  --include-namespaces rutas-norte-pre --default-volumes-to-fs-backup --wait

T0=$(date +%s)
kubectl delete namespace rutas-norte-pre
velero restore create rec-$(date +%s) --from-backup simulacro-pre-20260805 --wait
kubectl wait --for=condition=available --timeout=900s deployment --all -n rutas-norte-pre
kubectl exec -n rutas-norte-pre deploy/postgres-reservas -- \
  psql -U rutasnorte -d reservas -c "SELECT count(*), max(fecha) FROM reservas;"
echo "RTO: $(( ($(date +%s) - T0) / 60 )) minutos"

Acta tipo:

Campo Valor
Fecha / responsable 2026-08-05 / [email protected]
Copia usada simulacro-pre-20260805, fs-backup, 18,4 GB
RTO medido / RPO efectivo 23 min (borrado → primer 200 OK verificado) / 47 min
Objetos restaurados 5 Deployments, 5 Services, 2 Ingress, 6 NetworkPolicies, 4 Secrets, 2 PVC
No volvió solo El certificado TLS (reemitido en 4 min por cert-manager); la IP del Service LoadBalancer cambió; el Secret del SMTP se restauró con un token ya rotado
Incidencias Los PVC tardaron 6 min en aprovisionarse y montarse: el 40 % del RTO
Acciones (1) IP estática reservada para el Ingress; (2) rotación del token SMTP documentada en el runbook; (3) evaluar --snapshot-volumes para bajar el tiempo de los PVC

Valoración de la política: el RTO objetivo de pre era de 8 horas y el medido es de 23 minutos, así que se cumple con enorme margen. Pero la conclusión útil es otra: el mismo procedimiento aplicado a producción no daría 23 minutos, porque en pro el volumen es diez veces mayor, hay que actualizar DNS con su propagación, regenerar secretos externos y verificar el negocio de extremo a extremo. La acción correcta es repetir el simulacro sobre una copia de producción restaurada en un namespace aparte, que es lo que da el número real de las 2 horas comprometidas.

Ejercicio 3

A. Qué se copia:

Qué Método Frecuencia Retención Protege de
Manifiestos Git Cada cambio Ilimitada Error de configuración
Datos de postgres-reservas pg_dump -Fc cifrado a PVC Cada hora (15 min en temporada alta) 72 h Error humano reciente en los datos
Objetos + volúmenes de rutas-norte-pro Velero + snapshots CSI Diaria 02:00 7 días Borrado de objetos, despliegue fallido
Objetos, volúmenes y recursos de clúster Velero + fs-backup a otra región, con --include-cluster-resources Semanal domingo 03:00 90 días Pérdida de zona, de cuenta, corrupción antigua; reconstrucción en clúster nuevo
Volumen antes de cada cambio con riesgo Snapshot CSI Bajo demanda 72 h Migración de esquema fallida

B. Compromisos:

Escenario RPO Reservas perdidas (puente) RTO
Corrupción de datos detectada rápido 1 h hasta 400 ~30 min
Namespace destruido 24 h (o 1 h con el volcado) hasta 400 ~45 min
Pérdida del centro de datos 7 días en el peor caso, 24 h en el habitual hasta 67.200 ~4 h

La última fila es la respuesta a la pregunta del comité, y es incómoda a propósito: la copia que sobrevive a perder el centro de datos es la semanal, de modo que en el peor caso se perderían hasta siete días de reservas. Si eso no es aceptable —y no debería serlo—, hay dos inversiones que lo arreglan: pasar la copia externa a diaria (RPO de 24 h) y, sobre todo, archivar el WAL de PostgreSQL de forma continua a un almacén de objetos externo, que llevaría el RPO a minutos. Es la mejora prioritaria del plan.

C. Lo que no se restaura solo:

Elemento Responsable Acción
DNS de www.rutasnorte.example y api.rutasnorte.example Plataforma Actualizar registros e IP; contar con la propagación
Secretos externos: clave de pagos.proveedorexterno.example y token SMTP Seguridad Regenerar desde el gestor corporativo y reaplicar
Infraestructura: nodos, red, cortafuegos, StorageClasses, ingress-nginx, cert-manager Plataforma Infraestructura como código; crear las StorageClasses antes de restaurar

D. Datos personales:

Las copias contienen nombre, DNI, teléfono y correo de todos los clientes que han comprado un billete, además del historial completo de reservas. Protecciones aplicadas: cifrado en reposo de los volcados con clave pública (privada custodiada fuera del clúster), cifrado del bucket, cifrado en tránsito, credenciales de solo escritura para el proceso de copia, acceso a la restauración limitado por RBAC y registrado, retención automática por TTL y anonimización obligatoria de cualquier clon destinado a entornos no productivos.

Lo que debe validar el responsable de cumplimiento normativo, antes de poner nada de esto en producción:

  1. Que los plazos de retención (72 h, 7 días, 90 días) son los legalmente correctos para esta finalidad, ni más ni menos.
  2. Que la región del almacén de copias está dentro del ámbito geográfico permitido y declarado.
  3. El procedimiento ante el derecho de supresión: qué se hace con las copias que ya contienen los datos de quien lo ejerce, y cómo se documenta.
  4. La base legal del tratamiento y su reflejo en el registro de actividades, incluyendo las copias como tratamiento.
  5. El protocolo de notificación de brechas si una copia se ve comprometida.
  6. Las condiciones del encargado del tratamiento con el proveedor de la nube que aloja las copias.

Este documento es una propuesta técnica y no sustituye esa revisión.

Conclusión

Rutas Norte es ya una plataforma en la que se puede confiar. Sabes qué hay que salvar —los manifiestos, que ya están en Git; el estado de la API, con los Secrets y los PVC que Git no contiene; y los datos de los volúmenes, lo único verdaderamente irreemplazable— y que cada uno exige una técnica distinta. Tienes interiorizada la regla 3-2-1 y sabes que, de todas las copias de Rutas Norte, solo la semanal al almacén de objetos de otra región cumple el "1"; las demás son comodidad y velocidad. Y manejas RPO y RTO con números concretos: a 400 reservas por hora en un puente, una copia diaria significa perder hasta 9.600 reservas, y eso convierte una discusión técnica en una decisión de negocio que alguien tiene que tomar y firmar.

Has construido las dos piezas. La copia lógica: un CronJob horario que ejecuta pg_dump -Fc, verifica el volcado con pg_restore --list, lo cifra con una clave pública cuya privada no vive en el clúster, aplica retención local y —detalle que se olvida siempre— tiene su propia NetworkPolicy para atravesar el deny-all de producción. Y la copia de la plataforma con Velero: su arquitectura de controlador, node-agent y plugins; las dos vías para los volúmenes, con la diferencia decisiva de que solo la copia al almacén de objetos saca de verdad los datos del sistema de almacenamiento; los hooks pre y post con pg_backup_start/pg_backup_stop que resuelven la consistencia que dejamos abierta en 05-05, y la advertencia de que el post debe ejecutarse siempre so pena de llenar el disco de WAL; los Schedule con TTL que hacen la retención automática; y la restauración con --namespace-mappings para verificar antes de tocar nada real.

Y has hecho lo que separa una copia de una copia útil: el simulacro. Borrar rutas-norte-pre entero y recuperarlo con cronómetro, midiendo un RTO real y descubriendo la lista de lo que no vuelve solo —el certificado, la IP del LoadBalancer, el DNS, los secretos externos, los recursos de clúster—, que es literalmente imposible de conocer sin ejecutarlo. De ahí sale el runbook que alguien de guardia puede seguir a las tres de la mañana. Junto a todo ello queda la advertencia que no es técnica: las copias contienen datos personales de clientes, deben cifrarse, tener plazo de borrado, no salir del ámbito legal previsto, y la política de retención y el tratamiento de esos datos deben ser revisados y aprobados por el responsable de cumplimiento normativo antes de llevar nada de esto a producción.

Con esto termina el módulo 5. La deuda que arrastrábamos desde el módulo 2 está saldada por completo: postgres-reservas guarda sus datos en un volumen que sobrevive al pod, aprovisionado dinámicamente por una StorageClass con Retain, expandible en caliente, con snapshots antes de cada cambio arriesgado y con copias verificadas dentro y fuera del clúster.

Queda un techo que hemos encontrado tres veces y siempre hemos aplazado con la misma frase. En 05-03 descubriste que un Deployment tiene una sola plantilla, de modo que todas sus réplicas piden el mismo PVC y por eso postgres-reservas está condenado a replicas: 1 con strategy: Recreate. Una base de datos de producción necesita más: identidad estable, arranque ordenado y un volumen propio por réplica. Eso lo da el volumeClaimTemplate de los StatefulSets, y con él arranca el módulo 6, Conceptos Avanzados: StatefulSets, DaemonSets, Jobs y CronJobs —donde por fin llega informes-ocupacion, el sexto componente de la plataforma—, init containers y sidecars, planificación con afinidad y taints, recursos personalizados y operadores. Empezamos por StatefulSets.

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