StackPractices
advanced Por Mathias Paulenko

Plan de Pruebas de Recuperación ante Desastres

Una plantilla para planificar y ejecutar pruebas de recuperación ante desastres incluyendo validación de failover, verificación de integridad de datos y medición del tiempo de recuperación.

Descripción general

Un plan de recuperación ante desastres sin probar es solo una esperanza sin verificar. Cuando llega el desastre de verdad, los huecos aparecen rápido: dependencias que faltan, runbooks desactualizados, tiempos de recuperación que nadie midió nunca. Este plan te da una forma repetible de poner a prueba esos procedimientos — failovers controlados, verificación de integridad de datos y cifras honestas de RTO/RPO. Ejercita el documento que produce la plantilla de plan de recuperación ante desastres.

Cuándo usarla

Usa este plan cuando:

  • El cumplimiento exige una prueba de DR en el calendario (SOC2, ISO 27001)
  • La infraestructura o los procedimientos de recuperación cambiaron hace poco
  • Un incidente pasado expuso brechas en tus capacidades de DR
  • Les debes a las partes interesadas una prueba de que las cifras de RTO/RPO son reales

Requisitos previos

Antes de comenzar:

  • Runbooks de DR revisados y actualizados en los últimos 90 días
  • Entorno de prueba disponible (aislado de producción)
  • Snapshots de backup confirmados como recuperables en los últimos 7 días
  • Stakeholders notificados de la ventana de prueba e impacto esperado
  • Plan de rollback documentado en caso de que la prueba salga mal

Solución

# Plan de Prueba de Recuperación ante Desastres: `<Nombre del Servicio>`

## 1. Objetivos de la Prueba

| Objetivo | Meta | medición |
|----------|------|----------|
| Recovery Time Objective (RTO) | < 4 horas | Tiempo desde declaración de desastre hasta disponibilidad del servicio |
| Recovery Point Objective (RPO) | < 15 minutos | pérdida máxima de datos aceptable en recuperación |
| Integridad de Datos | 100% | Todas las transacciones verificadas contra la fuente |
| Comunicación | < 30 minutos | Todas las partes interesadas notificadas del inicio de la prueba |

## 2. Alcance y Suposiciones

### En Alcance
- Failover de base de datos y restauración desde backup
- Re-despliegue de aplicación a región de DR
- Cambio de DNS y enrutamiento de tráfico
- validación de smoke test de flujos criticos de usuario

### Fuera de Alcance
- Dependencias de servicios de terceros (asumidos disponibles)
- Fallas completas de centro de datos fisico (prueba basada en cloud)
- Escenarios de aislamiento completo de red

### Suposiciones
- El último snapshot de backup es válido y completo
- La región de DR tiene capacidad suficiente
- La conectividad de red entre regiones es funcional
- Los miembros del equipo estan disponibles durante la ventana de prueba

## 3. Escenarios de Prueba

### Escenario A: Falla Completa de región Primaria

**Disparador:** simulación de indisponibilidad completa de la región primaria de AWS.

**Pasos:**
1. **Declarar desastre** (0:00) — Ingeniero líder anuncia prueba de DR en canal de incidente
2. **Restaurar base de datos** (0:30) — Restaurar desde último snapshot en región de DR
3. **Desplegar aplicaciones** (1:00) — Desplegar stack de aplicación a cluster de DR
4. **Actualizar DNS** (1:30) — Cambiar tráfico a balanceador de carga de DR
5. **Validar** (2:00) — Ejecutar smoke tests y verificar integridad de datos
6. **Medir RTO** — Registrar tiempo desde declaración hasta validación exitosa

### Escenario B: corrupción de Base de Datos

**Disparador:** Base de datos primaria tiene datos corruptos requiriendo recuperación point-in-time.

**Pasos:**
1. Identificar punto de corrupción desde alertas de monitoreo
2. Restaurar base de datos a 5 minutos antes del timestamp de corrupción
3. Verificar integridad transaccional con consultas de checksum
4. Promover instancia restaurada a primaria
5. Redirigir tráfico de aplicación

## 4. Ejecución de la Prueba

### Checklist Pre-Prueba

| Item | Estado | Responsable |
|------|--------|-------------|
| Snapshot de backup creado | [ ] | DBA |
| Entorno de DR aprovisionado | [ ] | Plataforma |
| Runbooks impresos / accesibles offline | [ ] | SRE |
| Stakeholders notificados | [ ] | líder de Incidente |
| Plan de rollback confirmado | [ ] | SRE |
| Dashboards de monitoreo listos | [ ] | Observabilidad |

### Línea de Tiempo de Ejecución

| Tiempo | Actividad | Resultado Esperado |
|--------|-----------|-------------------|
| T+0:00 | Declarar inicio de prueba | Canal de incidente notificado |
| T+0:05 | Iniciar restauración de base de datos | Trabajo de restauración RDS iniciado |
| T+1:00 | Base de datos disponible | Prueba de conexión exitosa |
| T+1:30 | Desplegar aplicación | Todos los pods saludables |
| T+2:00 | Cambio de DNS | tráfico llegando a región de DR |
| T+2:30 | Smoke tests | 100% tasa de éxito |
| T+3:00 | verificación de integridad de datos | Conteo de transacciones coincide con fuente |
| T+4:00 | medición de RTO | Registrar actual vs meta |

### Procedimiento de Rollback

Si cualquier paso crítico falla:
1. Pausar prueba inmediatamente
2. Revertir DNS a región primaria
3. NO eliminar recursos de DR hasta revisión post-prueba
4. Escalar a gerente de ingenieria
5. Programar prueba de seguimiento después de correcciones

## 5. Verificación de Integridad de Datos

```sql
-- comparación de conteo de transacciones
SELECT 'primary' as source, COUNT(*) as tx_count FROM orders
UNION ALL
SELECT 'dr', COUNT(*) FROM dr_orders;

-- comparación de checksums
SELECT 'primary', SUM(CHECKSUM(*)) FROM payments
UNION ALL
SELECT 'dr', SUM(CHECKSUM(*)) FROM dr_payments;

-- último timestamp de transacción
SELECT MAX(created_at) FROM orders;
```

| Verificación | Primario | DR | Coincide |
|--------------|----------|-----|----------|
| Conteo de transacciones | ______ | ______ | [ ] |
| Checksum a nivel de fila | ______ | ______ | [ ] |
| último timestamp | ______ | ______ | [ ] |

## 6. Reporte Post-Prueba

### Resumen de Resultados

| Métrica | Meta | Actual | Estado |
|---------|------|--------|--------|
| RTO | < 4 horas | ______ | Aprobado / Fallido |
| RPO | < 15 minutos | ______ | Aprobado / Fallido |
| Integridad de datos | 100% | ______ | Aprobado / Fallido |
| Smoke tests | 100% | ______ | Aprobado / Fallido |

### Problemas Encontrados

| Problema | Severidad | Responsable | Fecha Limite de corrección |
|----------|-----------|-------------|---------------------------|
| ______ | crítica / Alta / Media / Baja | ______ | ______ |

### Lecciones Aprendidas

- Lo que funciono bien:
- Lo que tomo más tiempo de lo esperado:
- Lo que fallo inesperadamente:
- Lo que debería automatizarse:

### Aprobación

| Rol | Nombre | Fecha | Firma |
|-----|--------|-------|-------|
| Líder de Prueba | ______ | ______ | ______ |
| Gerente de Ingeniería | ______ | ______ | ______ |
| Compliance (si aplica) | ______ | ______ | ______ |

Explicación

El plan divide las pruebas de DR en objetivos declarados (metas de RTO/RPO), escenarios controlados (modos específicos de falla) y resultados medibles (criterios de aprobado/fallido). El checklist pre-prueba existe para que un simulacro no falle porque a alguien se le olvidó un snapshot — eso es un problema de preparación, no una brecha de DR. Y como las pruebas salen mal a veces, el procedimiento de rollback evita que un mal simulacro se convierta en una caída real.

Mermaid flowchart TD diagram

Cada marca de tiempo es un checkpoint, no una suposición — si la restauración de la base de datos pasa de T+1:00, la prueba ya sabe que falló antes de llegar al final.

Variantes

ContextoEnfoqueNotas
Prueba de cumplimiento anualEscenario completo con auditoresDocumentar todo, mantener firmas
Ejercicio interno trimestralEscenario abreviado, sin auditorEnfocarse en coordinación del equipo y tiempos
después de cambio de infraestructuraEscenario dirigido al componente cambiadoValidar solo lo que cambio
Game day / chaos engineeringPrueba no anunciadamás realista, requiere automatización madura

Lo que funciona

  1. Programar pruebas durante horario laboral — las personas que necesitan aprender deben participar
  2. Medir, no estimar — el RTO real casi siempre es mayor que la estimación documentada
  3. Probar desde backup, no solo desde replicación — la replicación puede estar saludable mientras los backups estan corruptos
  4. Documentar toda desviación — incluso diferencias menores de tiempo indican brechas de proceso
  5. Automatizar smoke tests — la verificación manual es demasiado lenta durante un desastre real

Errores comunes

  1. Probar solo failover, no restauración desde backup — la replicación puede estar saludable mientras los backups estan corruptos
  2. No probar con volúmenes de datos realistas — restaurar 1TB toma más que restaurar 1GB
  3. Saltar la prueba del procedimiento de rollback — descubres que el rollback no funciona cuando más lo necesitas
  4. Probar solo durante períodos de bajo tráfico — no valida las suposiciones de capacidad
  5. No actualizar los runbooks después de los hallazgos — las mismas brechas aparecen en la prueba del próximo año

Soluciones Avanzadas

Ejecución automatizada de pruebas de DR con Terraform y AWS

Aprovisiona el entorno de DR, restaura desde backup y ejecuta pruebas de validación automáticamente:

import boto3
import subprocess
import time
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime, timedelta

@dataclass
class DRTestResult:
    test_name: str
    start_time: datetime
    end_time: Optional[datetime] = None
    rto_seconds: Optional[int] = None
    rpo_seconds: Optional[int] = None
    steps_completed: List[str] = field(default_factory=list)
    steps_failed: List[str] = field(default_factory=list)
    data_integrity_ok: bool = False

class DRTestRunner:
    def __init__(self, region: str, dr_region: str):
        self.region = region
        self.dr_region = dr_region
        self.rds = boto3.client("rds", region_name=dr_region)
        self.ec2 = boto3.client("ec2", region_name=dr_region)
        self.s3 = boto3.client("s3", region_name=region)

    def restore_database_from_snapshot(
        self, snapshot_id: str, db_instance_id: str
    ) -> str:
        """Restore RDS instance from snapshot in DR region."""
        print(f"Restoring {db_instance_id} from snapshot {snapshot_id}...")
        response = self.rds.restore_db_instance_from_db_snapshot(
            DBInstanceIdentifier=db_instance_id,
            DBSnapshotIdentifier=snapshot_id,
            AvailabilityZone=f"{self.dr_region}a",
        )
        # Wait for instance to be available
        waiter = self.rds.get_waiter("db_instance_available")
        waiter.wait(DBInstanceIdentifier=db_instance_id)
        endpoint = response["DBInstance"]["Endpoint"]["Address"]
        print(f"Database available at: {endpoint}")
        return endpoint

    def run_smoke_tests(self, endpoint: str) -> bool:
        """Run smoke tests against the restored environment."""
        tests = [
            ("health_check", f"curl -sf http://{endpoint}/health"),
            ("write_test", f"curl -sf -X POST http://{endpoint}/api/test -d '{{\"test\":\"dr\"}}'"),
            ("read_test", f"curl -sf http://{endpoint}/api/test/dr"),
        ]
        all_passed = True
        for name, cmd in tests:
            result = subprocess.run(cmd, shell=True, capture_output=True, timeout=30)
            if result.returncode == 0:
                print(f"  PASS: {name}")
            else:
                print(f"  FAIL: {name}: {result.stderr.decode()}")
                all_passed = False
        return all_passed

    def verify_data_integrity(
        self, primary_conn: str, dr_conn: str
    ) -> bool:
        """Compare transaction counts between primary and DR."""
        primary_count = self._query_count(primary_conn)
        dr_count = self._query_count(dr_conn)
        match = primary_count == dr_count
        print(f"Primary: {primary_count} rows, DR: {dr_count} rows, Match: {match}")
        return match

    def _query_count(self, conn_str: str) -> int:
        """Execute a count query and return the result."""
        cmd = f'psql "{conn_str}" -t -c "SELECT COUNT(*) FROM orders;"'
        result = subprocess.run(cmd, shell=True, capture_output=True, timeout=60)
        return int(result.stdout.decode().strip()) if result.returncode == 0 else -1

    def execute_full_test(self, snapshot_id: str) -> DRTestResult:
        """Execute a full DR test and return results."""
        result = DRTestResult(
            test_name="full_region_failure",
            start_time=datetime.now(),
        )

        try:
            # Step 1: Restore database
            endpoint = self.restore_database_from_snapshot(
                snapshot_id, "dr-test-instance"
            )
            result.steps_completed.append("database_restore")

            # Step 2: Run smoke tests
            if self.run_smoke_tests(endpoint):
                result.steps_completed.append("smoke_tests")
            else:
                result.steps_failed.append("smoke_tests")

            # Step 3: Verify data integrity
            if self.verify_data_integrity(
                "postgresql://primary.db.internal/appdb",
                f"postgresql://{endpoint}/appdb",
            ):
                result.data_integrity_ok = True
                result.steps_completed.append("data_integrity")
            else:
                result.steps_failed.append("data_integrity")

        except Exception as e:
            print(f"Test failed: {e}")
            result.steps_failed.append(f"exception: {str(e)}")

        result.end_time = datetime.now()
        result.rto_seconds = int((result.end_time - result.start_time).total_seconds())
        return result

# Example usage
runner = DRTestRunner("us-east-1", "us-west-2")
result = runner.execute_full_test("rds-snapshot-2026-07-01")
print(f"\nRTO: {result.rto_seconds}s")
print(f"Steps completed: {result.steps_completed}")
print(f"Steps failed: {result.steps_failed}")
print(f"Data integrity: {result.data_integrity_ok}")

Script de game day de chaos engineering

Inyecta fallas controladas para probar la preparación de DR sin aviso previo. Advertencia: el escenario full hace cordon y drain de todos los nodos con la etiqueta de la región primaria — ejecútalo solo en clusters que puedas permitirte tumbar, y nunca como primer simulacro.

#!/bin/bash
# game-day.sh - Chaos engineering DR test
# Usage: ./game-day.sh --scenario <network|disk|cpu|full>

set -euo pipefail

SCENARIO="${1:---scenario}"
shift || true

case "$SCENARIO" in
  --scenario) SCENARIO="$1" ;;
esac

NAMESPACE="production"
SERVICE="api"
LOG_FILE="/tmp/game-day-$(date +%s).log"

log() { echo "[$(date -u +%H:%M:%S)] $1" | tee -a "$LOG_FILE"; }

case "$SCENARIO" in
  network)
    log "Injecting network latency to ${SERVICE} pods..."
    kubectl exec -n "$NAMESPACE" deployment/"$SERVICE" -- \
      tc qdisc add dev eth0 root netem delay 500ms 2>/dev/null || true
    log "Monitoring for 5 minutes..."
    sleep 300
    kubectl exec -n "$NAMESPACE" deployment/"$SERVICE" -- \
      tc qdisc del dev eth0 root 2>/dev/null || true
    log "Network latency removed."
    ;;

  disk)
    log "Filling disk on ${SERVICE} pods to 90%..."
    kubectl exec -n "$NAMESPACE" deployment/"$SERVICE" -- \
      fallocate -l $(df --output=avail -BG / | tail -1 | tr -d 'G ')M /tmp/fill 2>/dev/null || true
    sleep 120
    kubectl exec -n "$NAMESPACE" deployment/"$SERVICE" -- rm -f /tmp/fill 2>/dev/null || true
    log "Disk space restored."
    ;;

  cpu)
    log "Spiking CPU on ${SERVICE} pods..."
    kubectl exec -n "$NAMESPACE" deployment/"$SERVICE" -- \
      sh -c 'yes > /dev/null & yes > /dev/null & yes > /dev/null &' 2>/dev/null || true
    sleep 120
    kubectl delete pod -n "$NAMESPACE" -l app="$SERVICE" --grace-period=0 --force 2>/dev/null || true
    log "CPU spike ended (pods restarted)."
    ;;

  full)
    log "Starting full region failure simulation..."
    log "1. Cordoning all nodes in primary region..."
    kubectl cordon --selector=topology.kubernetes.io/region=us-east-1
    log "2. Draining workloads..."
    kubectl drain --selector=topology.kubernetes.io/region=us-east-1 \
      --ignore-daemonsets --delete-emptydir-data --timeout=300s || true
    log "3. Waiting 5 minutes for failover..."
    sleep 300
    log "4. Checking DR region health..."
    kubectl get pods -n production --context=dr-cluster
    log "5. Uncordoning primary region..."
    kubectl uncordon --selector=topology.kubernetes.io/region=us-east-1
    log "Full region simulation complete."
    ;;

  *)
    echo "Usage: $0 --scenario <network|disk|cpu|full>"
    exit 1
    ;;
esac

log "Game day results saved to $LOG_FILE"

Query de dashboard de monitoreo RTO/RPO

Rastrea métricas de RTO y RPO a lo largo del tiempo para identificar tendencias y degradación:

-- Prometheus query for RPO tracking (replication lag over time)
-- Use in Grafana dashboard
SELECT
  date_trunc('day', timestamp) as day,
  avg(value) as avg_lag_seconds,
  max(value) as max_lag_seconds,
  percentile_cont(0.99) WITHIN GROUP (ORDER BY value) as p99_lag_seconds
FROM prometheus_metrics
WHERE metric_name = 'pg_replication_lag_seconds'
  AND timestamp > now() - interval '90 days'
GROUP BY 1
ORDER BY 1;

-- DR test results over time
SELECT
  test_date,
  rto_target_seconds,
  rto_actual_seconds,
  rpo_target_seconds,
  rpo_actual_seconds,
  CASE
    WHEN rto_actual_seconds <= rto_target_seconds THEN 'PASS'
    ELSE 'FAIL'
  END as rto_status,
  CASE
    WHEN rpo_actual_seconds <= rpo_target_seconds THEN 'PASS'
    ELSE 'FAIL'
  END as rpo_status
FROM dr_test_results
ORDER BY test_date DESC
LIMIT 12;

Calendario de pruebas de DR

TrimestreEscenarioResponsableRTO objetivoÚltima prueba
Q1 2026Fallo de región primariaEquipo de plataforma4 horas2026-01-15
Q2 2026Corrupción de base de datosEquipo DBA2 horas2026-04-20
Q3 2026Partición de redEquipo de red1 horaPendiente
Q4 2026Región completa + restauración desde backupEquipo SRE8 horasPendiente

Un calendario por sí solo no hace real el plan:

  1. Recorre la línea de tiempo en papel primero. Haz un pase de mesa sobre el cronograma de ejecución antes de tocar infraestructura — los huecos en responsables y accesos salen gratis.
  2. Incluye pruebas de dependencias de terceros. Proveedores de DNS, CDNs, APIs externas — tu plan de DR depende de todos ellos, así que su failover también necesita un simulacro:
# Prueba de failover de DNS
dig @8.8.8.8 app.example.com +short
# Debería devolver la IP de la región de DR tras el corte

# Prueba de failover de CDN
curl -sI https://app.example.com | grep "x-served-by"
# Debería mostrar el nodo de caché de DR tras el corte

# Prueba del circuit breaker de dependencia de API externa
curl -X POST http://app.internal/admin/test-circuit-breaker \
  -d '{"dependency":"payment-gateway","action":"open"}'

Ver también

Preguntas frecuentes

¿Con qué frecuencia deberíamos ejecutar pruebas de DR?

Lo anual es el suelo de cumplimiento; lo trimestral mantiene al equipo honesto. Cada cambio de infraestructura que toque una ruta de recuperación se gana su propio simulacro. Los equipos con automatización madura van más allá y corren ejercicios de caos mensuales.

¿Cómo medimos el RTO con honestidad?

Desde la declaración de desastre hasta el servicio validado, no desde que arranca el script. El tiempo de detección cuenta: si el monitoreo tardó 4 minutos en avisar a alguien, esos 4 minutos son parte de tu RTO. Mide la versión que viven tus usuarios.

¿Por qué probar restauraciones desde backup y no solo failover?

La replicación puede estar perfectamente sana mientras tus backups están corruptos en silencio. Una prueba de failover demuestra que la réplica funciona; una de restauración demuestra que sobrevives al día en que la réplica no existe. Ejecuta ambas, en calendarios distintos.

¿Qué pasa si la prueba de DR falla catastróficamente?

Esa es información valiosa. La prueba ha revelado que tu plan de DR no funciona — mejor descubrirlo en una ventana controlada que durante un desastre real. Pausa la prueba, restaura producción, corrige los problemas y reprograma.

¿Necesitamos notificar a clientes sobre pruebas de DR?

Si la prueba toca algo que los clientes pueden ver, sí — prográmala dentro de una ventana de mantenimiento y avisa con antelación. Si es solo interna, no hay nadie fuera del equipo a quien avisar.

¿Quién debería ser responsable de una prueba de DR?

Un líder de prueba con nombre y con autoridad para dar por terminado el simulacro. Rotar ese rol entre trimestres funciona bien; lo que no funciona es un simulacro sin un único dueño — esos terminan en un informe que nadie abre.