сделать disaster recovery воспроизводимым, тестируемым и привязанным к RTO и RPO

Disaster Recovery as Code: автоматизация RTO (время восстановления) и RPO (точка восстановления) через шаблоны

13 минут

RTO ограничивает допустимый downtime сервиса, RPO — допустимую потерю данных. В материале — оба target в Terraform, верификация backup, failover-инфраструктура и orchestration recovery через проверенные pipeline.

Что такое RTO и RPO и зачем они нужны в disaster recovery

Recovery Time Objective (RTO) — максимально допустимое время простоя после сбоя: как быстро сервис должен снова работать. Recovery Point Objective (RPO) — максимально допустимая потеря данных: насколько далеко назад вы готовы откатиться при восстановлении. Если RPO — один час, backup или репликация должны быть не старше; если RTO — четыре часа, runbook'и, failover stack и drill'ы должны доказывать восстановление в этом окне. Эти два числа задают cadence backup, бюджет multi-region и глубину orchestration — они превращают размытое «нам нужен DR» в измеримые инженерные targets, которые можно проверить на game day.

Почему ручные DR-планы проваливаются в реальных инцидентах

У большинства команд есть документ disaster recovery, написанный один раз и почти не проверяемый. Когда падает production, runbook ссылается на выведенные из эксплуатации сервисы, формат backup изменился незаметно, а порядок восстановления живёт в чьей-то памяти. Четыре пробела повторяются везде: документация расходится с live-инфраструктурой, restore не замеряют end to end, RTO и RPO различаются между командами без измерений, шаги cutover — tribal knowledge. Disaster Recovery as Code (DRaC) переносит дисциплину IaC на recovery: versioned templates, automated drill'ы, auditable changes и явные targets, которые можно тестировать.

Строительные блоки DRaC: targets, topology, verification, orchestration

Объявить RTO и RPO в коде — не значит их гарантировать: оба доказываются timed drill'ами. Топологию recovery выбирают осознанно. Backup and restore — минимальная стоимость и самый высокий RTO. Pilot light держит минимальные DR-ресурсы и масштабируется при failover. Warm standby постоянно крутит урезанный replica stack. Multi-region active-passive или active-active дороже, но сжимает RTO и RPO. Объявляйте targets versioned variables, поднимайте DR-инфраструктуру из тех же modules что production с вариантами region или account, верифицируйте backup scheduled restore test'ами и оркестрируйте cutover явным dependency graph вместо ad hoc runbook.

Объявите RTO и RPO и выровняйте cadence backup

Храните RTO и RPO как Terraform variables с review как у production change. Маппите RPO на backup schedule, который AWS Backup реально поддерживает — обычно cron expression, а не произвольный rate в минутах. Organizations backup policies полезны на enterprise-масштабе; aws_backup_plan — понятнее для application-команд. Зафиксируйте: выполнение RPO требует replication или backup frequency ниже target плюс мониторинг возраста last-successful-backup.

HCL · переменные RTO/RPO и AWS Backup plan
variable "rto_minutes" {
  description = "Maximum acceptable service downtime in minutes"
  type        = number
  default     = 60
}

variable "rpo_minutes" {
  description = "Maximum acceptable data loss window in minutes"
  type        = number
  default     = 60
}

locals {
  # Align schedule to the nearest practical cadence for your RPO
  backup_schedule = var.rpo_minutes <= 60 ? "cron(0 * * * ? *)" : "cron(0 0 * * ? *)"
}

resource "aws_backup_vault" "dr" {
  name = "application-dr-vault"
}

resource "aws_backup_plan" "application" {
  name = "application-dr-plan"

  rule {
    rule_name         = "rpo-aligned-backup"
    target_vault_name = aws_backup_vault.dr.name
    schedule          = local.backup_schedule

    lifecycle {
      delete_after = 35
    }
  }
}

Автоматизируйте restore drill'ы с замером RPO и RTO

Backup без restore test — это стоимость хранения. Запускайте изолированные drill'ы с restore в throwaway instance name, мерьте elapsed time для database availability waiter и проверяйте snapshot age против RPO. Полный RTO включает DNS propagation и application warm-up — явно ограничивайте scope метрик drill, чтобы команда не путала database restore time с end-to-end service recovery. Публикуйте результаты в CloudWatch или incident metrics store и алертьте при failure.

Python · RDS restore drill с проверкой RPO и частичного RTO
import time
from datetime import datetime, timezone

import boto3


class BackupVerifier:
    def __init__(self, rto_minutes=60, rpo_minutes=60):
        self.rto_minutes = rto_minutes
        self.rpo_minutes = rpo_minutes
        self.rds = boto3.client("rds")

    def test_restore(self, source_instance_id, drill_instance_id):
        start = time.time()

        snapshots = self.rds.describe_db_snapshots(
            DBInstanceIdentifier=source_instance_id,
            SnapshotType="automated",
        )["DBSnapshots"]

        if not snapshots:
            raise RuntimeError("No automated snapshots found")

        latest = max(snapshots, key=lambda item: item["SnapshotCreateTime"])
        snapshot_time = latest["SnapshotCreateTime"]
        if snapshot_time.tzinfo is None:
            snapshot_time = snapshot_time.replace(tzinfo=timezone.utc)

        snapshot_age = (
            datetime.now(timezone.utc) - snapshot_time
        ).total_seconds() / 60

        if snapshot_age > self.rpo_minutes:
            raise AssertionError(
                f"RPO breach: snapshot age {snapshot_age:.1f}m exceeds {self.rpo_minutes}m"
            )

        self.rds.restore_db_instance_from_db_snapshot(
            DBInstanceIdentifier=drill_instance_id,
            DBSnapshotIdentifier=latest["DBSnapshotIdentifier"],
            DBInstanceClass="db.r6g.large",
            MultiAZ=False,
            PubliclyAccessible=False,
            DeletionProtection=False,
        )

        waiter = self.rds.get_waiter("db_instance_available")
        waiter.wait(DBInstanceIdentifier=drill_instance_id)

        elapsed = (time.time() - start) / 60
        if elapsed > self.rto_minutes:
            raise AssertionError(
                f"RTO breach: restore took {elapsed:.1f}m, target {self.rto_minutes}m"
            )

        return {
            "status": "PASS",
            "rto_actual_minutes": round(elapsed, 1),
            "rpo_actual_minutes": round(snapshot_age, 1),
            "snapshot_id": latest["DBSnapshotIdentifier"],
        }

DR-инфраструктура и DNS failover as code

Определяйте secondary region или account stack как module variant production и синхронизируйте через те же CI gates. Scheduled terraform plan в DR workspace ловит template drift до инцидента. Route 53 failover routing переключает трафик при падении primary health check — используйте fqdn-based HTTPS check или ALB target health evaluation с alias records. DNS TTL и resolver caching всё равно добавляют минуты к cutover; закладывайте это в RTO budget.

GitHub Actions · plan и controlled apply DR stack
name: DR Infrastructure Sync
on:
  push:
    paths: ['infrastructure/**']
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  dr-sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_wrapper: false

      - name: Terraform plan (DR region)
        working-directory: infrastructure/dr
        run: |
          terraform init -input=false
          terraform plan -detailed-exitcode -input=false -var="environment=dr" -out=dr.tfplan

      - name: Apply DR infrastructure
        if: github.ref == 'refs/heads/main'
        working-directory: infrastructure/dr
        run: terraform apply -input=false dr.tfplan
HCL · Route 53 failover records для primary и DR ALB
resource "aws_route53_health_check" "primary" {
  fqdn              = var.primary_healthcheck_fqdn
  port              = 443
  type              = "HTTPS"
  resource_path     = "/health"
  failure_threshold = 3
  request_interval  = 30

  tags = {
    Name = "primary-api-health-check"
  }
}

resource "aws_route53_record" "failover_primary" {
  zone_id        = var.zone_id
  name           = "api.example.com"
  type           = "A"
  set_identifier = "primary"

  failover_routing_policy {
    type = "PRIMARY"
  }

  alias {
    name                   = var.primary_alb_dns
    zone_id                = var.primary_alb_zone_id
    evaluate_target_health = true
  }

  health_check_id = aws_route53_health_check.primary.id
}

resource "aws_route53_record" "failover_secondary" {
  zone_id        = var.zone_id
  name           = "api.example.com"
  type           = "A"
  set_identifier = "secondary"

  failover_routing_policy {
    type = "SECONDARY"
  }

  alias {
    name                   = var.dr_alb_dns
    zone_id                = var.dr_alb_zone_id
    evaluate_target_health = true
  }
}

Оркестрация recovery с явным dependency graph

Свяжите validation, data restore, cache warm-up, DNS cutover и post-cutover health checks в workflow, который можно запускать на game day. Argo Workflows подходит Kubernetes-эстейтам; Step Functions или CI workflow — для более простых stack'ов. Оставьте human approval gates для объявления disaster и production DNS failover — это policy decisions, не полностью автономные действия.

YAML · Argo Workflows disaster recovery DAG
apiVersion: argoproj.io/v1alpha1
kind: Workflow
metadata:
  name: disaster-recovery
spec:
  entrypoint: recover
  templates:
    - name: recover
      dag:
        tasks:
          - name: validate-dr-infra
            template: validate-infra
          - name: restore-databases
            template: restore-db
            dependencies: [validate-dr-infra]
          - name: sync-cache-layer
            template: sync-cache
            dependencies: [restore-databases]
          - name: update-dns
            template: dns-failover
            dependencies: [sync-cache-layer]
          - name: verify-recovery
            template: health-check
            dependencies: [update-dns]

    - name: validate-infra
      container:
        image: registry.example.com/dr-tools:latest
        command: [python, /scripts/validate_dr_infra.py]

    - name: restore-db
      container:
        image: registry.example.com/dr-tools:latest
        command: [python, /scripts/restore_database.py, --from-latest-snapshot, --verify-rpo]

    - name: sync-cache
      container:
        image: registry.example.com/dr-tools:latest
        command: [python, /scripts/warm_cache.py]

    - name: dns-failover
      container:
        image: registry.example.com/dr-tools:latest
        command: [python, /scripts/failover_dns.py]

    - name: health-check
      container:
        image: registry.example.com/dr-tools:latest
        command: [python, /scripts/verify_recovery.py]

Операционные практики, которые держат DR правдоподобным

Проводите timed game day ежемесячно в изолированном account или region, а не раз в год по слайдам. Версионируйте DR templates рядом с production и проверяйте DR impact в том же pull request при добавлении data store. Алертьте, когда возраст last successful backup превышает порог из RPO. Храните backup в immutable object storage вроде S3 Object Lock как защиту от ransomware. Подбирайте DR spend по размеру: pilot light или warm standby для большинства workload, active-active только когда бизнес требует sub-minute RTO. Аудируйте encryption, retention и cross-region copy через AWS Config или OPA непрерывно. Задокументируйте, кто может объявить disaster и когда fail over versus fail back. DR — ongoing practice; DRaC делает её тестируемой, а не теоретической.

RTO и RPO — это явные обязательства по надёжности; их задаём в гайде по SLO, SLI и error budget для платформенных команд.

Recovery drill'ы естественно сочетаются с controlled failure practice из playbook по Chaos Engineering для DevOps.