ADR-033 · DR · backup automation + offsite · RTO 1h / RPO 24h¶
Fecha: 2026-05-03 Source:
/srv/projects/cis/cis-plan/DECISIONS.md(do not edit here — re-split desde la fuente)
Contexto
Hoy los backups CIS son manuales y esporádicos: 5 archivos .sql.gz en /srv/projects/cis/backups/ (cis_admin · cis_platform · cis_inbox · cis_claudia, mezcla de ts del 2026-05-03). Falta:
- Automatización (un humano olvida un backup y queda hueco indefinido).
- Offsite (todo vive en el mismo disco que protege — pérdida de host = pérdida total).
- RTO/RPO declarados (sin objetivos no hay forma de medir si el procedimiento es aceptable).
- Drill (nunca se ejecutó un restore desde backup; la utilidad real de los
.sql.gzestá sin verificar). - Cobertura más allá de DBs (vault, Caddy, systemd units son recursos críticos sin backup).
Decisión
- RTO target 1h — desde "host vps-cis caído" hasta servicios CIS (cis-core, cis-admin, Caddy, Postgres) sirviendo tráfico. Lograble si: host nuevo pre-provisionado, offsite activo, runbook §5.2 ejecutado.
- RPO target 24h — backup nocturno único. Reducción a 6h o 1h queda para V2 vía WAL archiving (postpuesto post-cesión cochid ADR-030).
- Backup orchestrator en
cis-core/app/services/dr_backup.pycon 4 targets: backup_all_databases()· pg_dump por DB canónica (ADR-031: cis_admin · cis_platform · cis_claudia · cis_inbox · cis_pagos · cis_monitoreo) · gzip · ts UTC · retención 30 d.backup_vault()· tar.gz devault.key + projects/*.jsonFernet-encriptados + sha256 manifest.backup_caddy_config()· tar.gzCaddyfile + sites.d/ + snippets/.backup_systemd_units()· tar.gzcis-*.{service,timer}.- Cron systemd:
cis-backup.timer(OnCalendar*-*-* 03:00:00 UTC, RandomizedDelaySec=300, Persistent=true) →cis-backup.service(oneshot, user=illanes00, group=infra). - Restore drill
scripts/dr-restore-drill.shque crea DB temp<src>_drill, restaura último backup, valida row count ≥ 90% del fuente, drop. Cadencia semestral (mayo + noviembre). - Offsite — decisión humana pendiente entre Backblaze B2 (recomendación, ~USD 5/mes), Hetzner Storage Box (€3.20/mes), Wasabi S3. NO subir antes de aprobación humana + cuenta + TOS firmado. Volumen estimado 50 MB/noche hoy → 300 MB/noche end of 2026.
- Logging: structlog JSON (ADR-019) con events
dr_backup.run_start/db_ok/db_failed/vault_ok/caddy_ok/systemd_ok/run_complete.
Alternativas descartadas
borgcon repo offsite directo — más sofisticado (dedup, encrypted), pero overkill para 50 MB/noche y agrega dependencia que el equipo no maneja todavía. Reconsiderar en V2.- Backup vía pgBackRest — solo cubre Postgres, no vault/Caddy/systemd. Útil si se adopta WAL archiving en V2 pero no resuelve el alcance completo hoy.
- Snapshot del VPS al nivel hypervisor (HostingBy ofrece snapshot diario) — entrega RPO 24h sin esfuerzo, pero es opaco (no podés restaurar una sola DB), depende del proveedor, y no cumple "offsite real" porque vive en el mismo datacenter.
- rsnapshot a otro VPS propio — implica mantener un segundo host sin valor adicional vs B2.
Consecuencias positivas
- Cualquier pérdida diaria se acota a 24h (RPO declarado, antes era ∞).
- Restauración determinista vía runbook DR-PLAN.md §5.
- Cobertura completa: DBs + vault (secrets) + Caddy (TLS / routes) + systemd (service definitions). Permite reconstruir el host casi entero desde tarballs + GitHub mirror del código.
- Drill semestral asegura que los backups son restaurables (problema típico: backups que silenciosamente fallan).
- Logs estructurados ADR-019 → cualquier fallo de backup se ve en
journalctl -u cis-backupcon event names parseables.
Consecuencias negativas / gotchas
- El vault.key se incluye en el tar — el tar offsite es un secreto top-tier. Cifrado server-side obligatorio antes de subir a B2/Wasabi (lifecycle: enable encryption-at-rest del bucket).
- Authentik (sqlite) NO está cubierto todavía. Pérdida total = re-seed de sesiones + reauth de todos los usuarios. Migración a Postgres es deuda ADR-024.
- WAL archiving ausente: si una DB se corrompe a las 22:00, perdemos las últimas 19h de transacciones. Aceptable hoy (volumen bajo); revisitar cuando cis-pagos / cis-inbox crezcan.
cis_monitoreoaún no existe como DB — el dump fallará condatabase does not exist. Tratado como warning (no aborta el run); cuando se cree, se incluirá automáticamente.- Sin offsite hoy: si vps-cis pierde su disco, los backups locales se pierden con él. El target RTO 1h NO se cumple hasta que offsite esté activo.
Implementación
/srv/projects/cis/cis-core/app/services/dr_backup.py— orchestrator (creado · agent-LH-dr)./etc/systemd/system/cis-backup.{service,timer}— cron systemd (instalado + enabled · agent-LH-dr)./srv/projects/cis/scripts/dr-restore-drill.sh— drill (creado, ejecutable, NO ejecutado contra prod aún)./srv/projects/cis/DR-PLAN.md— RTO/RPO + runbook + checklist (creado · agent-LH-dr).- Pendiente humano: elegir proveedor offsite (recomendación B2), abrir cuenta, configurar
rclonepost-cis-backup.service. - Pendiente humano: ejecutar primer drill (
sudo /srv/projects/cis/scripts/dr-restore-drill.sh cis_admin) en ventana baja.
Verificación
systemctl list-timers cis-backup.timermuestra NEXT en próxima 03:00 UTC.journalctl -u cis-backup.service --since "2 days ago"muestradr_backup.run_complete ok=truecada noche.ls -lh /srv/projects/cis/backups/{db,vault,caddy,systemd}/ | tailmuestra archivos del día actual.find /srv/projects/cis/backups -mtime +30retorna vacío (retención efectiva).- Drill semestral exit 0 con log
DRILL OK · cis_admin restore verified.
Relaciona
- ADR-019 (logging spec, events
dr_backup.*siguen el contrato). - ADR-024 (Authentik separation — define cuándo Authentik DB entra al backup orchestrator).
- ADR-026 (operations permission model — restore manual contra prod =
restrictedop, requiere FES). - ADR-030 (cesión cochid — limita V2 WAL archiving timing).
- ADR-031 (DB universal access — pg_dump usa
:5432directo, único cliente legítimo en session-mode). - CONSTITUTION §2 infra canon ·
/srv/projects/amanual.