Skip to content
Stribog

Resilience

All writing

Disaster Recovery in Cloud Computing: DRaaS vs Owned Cluster

Disaster recovery in cloud computing moves your recovery point into a control plane you don't run. Azure Site Recovery and AWS DRS against an owned restore.

Stribog13 min read

The standard answer to this query is a product page: replicate to a second region, pay per protected server, and recovery becomes somebody else's runbook. That answer is not wrong, only incomplete: it quotes the recovery point objective a service can reach and leaves out the parameters, documented in the vendor's own FAQ and quota tables, that decide the recovery point you get. What follows reads those parameters for two named services, beside the one DR path a Kubernetes operator can own end to end.

What Disaster Recovery in Cloud Computing Is Actually Selling

Start with the unit of recovery, because it decides everything downstream. AWS describes the Elastic Disaster Recovery replication agent as one that "resides in memory and captures blocks continuously as they are written to disk", shipping them to a staging area, and says the service "facilitates RPOs of seconds" and "RTOs of minutes". Azure Site Recovery, in its Azure-to-Azure scenario, replicates Azure VMs into a target region. In both, the thing recovered is a server: a set of disks brought up at a point in time by the vendor's orchestrator.

An owned Kubernetes recovery path works in a different unit. etcd holds every API object the cluster has; Velero captures resources through the API and moves persistent-volume data out through CSI snapshots. What comes back is declarative cluster state, reconstituted into a cluster — not necessarily the one it left.

Neither unit is better; they fail differently. A block-replicated server knows nothing about the Deployment, PersistentVolumeClaim or etcd member it hosts, so a DRaaS-protected cluster is a fleet of machines whose disks must land at compatible points. A declarative restore knows exactly what it restores and nothing outside the cluster. The real choice is who holds each path's parameters.

The boundary does not disappear when you buy DR. On the left it sits just below the agent; on the right it sits at a bucket you run.

Where the Recovery Point Is Really Set

"RPOs of seconds" describes the distance between a source and its newest replicated state. It says nothing about the disasters replication copies faithfully: disks encrypted by ransomware, a schema migration that dropped the wrong column, a kubectl delete namespace against the wrong context. Those arrive at the target at replication speed. After a logical disaster the recovery point that matters is the newest clean one, and which points exist is decided by whoever runs the point set.

Azure Site Recovery documents that decision precisely. For Azure-to-Azure replication, "Site Recovery creates crash-consistent recovery points every five minutes by default. This setting can't be modified." That is a pin, not a floor: the spacing cannot be made finer, or coarser. App-consistent points are separate — "Site Recovery can create app-consistent recovery points with a minimum frequency of one hour" — and the default replication policy retains recovery points for one day with app-consistent snapshots "disabled and aren't created by default". A team that never edits the policy steps back through five-minute crash-consistent points for one day and has no application-consistent point at all. Other Site Recovery scenarios document different parameters; these come from the Azure-to-Azure FAQ.

Scope is the next term. Multi-VM consistency gives machines a shared crash-consistent point, and "you can replicate 16 virtual machines together in a replication group." What follows comes from that cap, not from anything Microsoft says about Kubernetes: a cluster whose control plane, workers and storage nodes add up to more than 16 VMs cannot put every disk in one replication group, so some machines recover at different instants from others.

The owned path has terms too, and all of them belong to the operator. A Velero Schedule is "a repeatable request for the Velero server to perform a backup for a given cron notation"; the server waits "for the next valid point of the given cron expression". A Velero-only recovery point is therefore bounded by the cron interval plus the time the backup takes to finish, and the etcd snapshot interval is a second timer you also write. Neither has a good default. Both change in a pull request.

Replication lag is the term vendors lead with. After a logical disaster, the terms that decide the answer are spacing, reach and scope — and on the replication path two of those three are not yours.

The Limits That Only Show Up in the Quota Table

AWS publishes Elastic Disaster Recovery's service quotas in a table with an Adjustable column. The rows that shape a regional recovery all read No: 300 replicating source servers per AWS account per Region, 100 source servers in a single job, 500 source servers across all jobs, and 20 concurrent jobs in progress.

Read those as part of the recovery plan. A fleet of 250 source servers needs at least three jobs, and every server in flight counts against the 500 ceiling across all jobs — a sequencing constraint on your RTO that exists before anyone writes a runbook.

Nor is 300 an absolute ceiling on protection. AWS's FAQ describes recovering more than 300 servers into a single target account by replicating through multiple staging accounts. That works, and it turns a quota into an account topology: more accounts to govern, audit and keep consistent.

Azure Site Recovery's Azure-to-Azure FAQ carries the equivalent post-failover work. Failed-over VMs "start up in the target disaster recovery region in an unprotected state"; reprotecting them means enabling replication back. Extensions "aren't replicated to the failover VM in the target region" and are installed manually. Capacity is explicitly not promised: "While Site Recovery makes a best effort to ensure that capacity is available in the recovery region, it does not guarantee the same." That best effort "is backed by a 1-hour RTO SLA", and guaranteed compute capacity means separately purchased Capacity Reservations.

bash
#!/usr/bin/env bash
# drs-headroom.sh: replicating source servers against the documented AWS DRS
# quota of 300 per account per Region (not adjustable). Run per staging account.
set -euo pipefail

QUOTA=300
(($# > 0)) || { echo "usage: drs-headroom.sh <region> [region ...]" >&2; exit 64; }

for region in "$@"; do
  aws drs describe-source-servers --region "$region" --output json |
    jq -r --arg r "$region" --argjson q "$QUOTA" '
      [ .items[] | (.dataReplicationInfo.dataReplicationState // "UNKNOWN") ] as $s
      | ($s | map(select(. != "STOPPED")) | length) as $n
      | ($s | group_by(.) | map("\(.[0])=\(length)") | join(",")) as $by
      | "\($r)\treplicating=\($n)\tquota=\($q)\theadroom=\($q - $n)\t\($by)"'
done
drs-headroom.sh — replicating source servers per Region against the documented, non-adjustable quota. The quota table does not define which replication states count, so this counts every server not in `STOPPED`, deliberately conservative, and prints the per-state breakdown for you to judge.

What an Owned Restore Actually Looks Like

The build — installing Velero, snapshot classes, schedules per workload tier — is covered in Kubernetes disaster recovery with Velero and etcd. What matters for this comparison is the restore's semantics, and three of them catch runbooks out.

First, an etcd restore is not a live operation. Kubernetes' documentation is blunt: if any API servers are running, "you should not attempt to restore instances of etcd". Stop all API server instances, restore state in all etcd instances, restart all API server instances — and, it recommends, restart kube-scheduler, kube-controller-manager and kubelet so none of them relies on stale data.

Second, a restore builds a new cluster rather than repairing the old one. Per etcd's v3.6 recovery guide, etcdutl snapshot restore "creates new etcd data directories; all members should restore using the same snapshot", and it overwrites the member ID and cluster ID so a restored member cannot join the old cluster — "the restore must start a new logical cluster." The same guide adds --bump-revision, so revisions never decrease after a restore, and --mark-compacted, so watches terminate and Kubernetes controllers' informer caches are invalidated rather than trusted.

Third, the command changed. The etcd 3.6 changelog lists etcdctl snapshot restore as removed. The Kubernetes task page still calls it deprecated and "slated for removal from etcd v3.6" — the sources disagree in tense, and the changelog is the authority. A runbook written for etcd 3.5 that calls etcdctl snapshot restore fails on any current etcd; the latest release as of 2026-09-13 is v3.7.1, published 2026-07-23.

bash
#!/usr/bin/env bash
# owned-restore.sh: kubeadm-style stacked etcd, etcd 3.6 or later.
# Run each phase on EVERY control-plane node before starting the next one:
#   snapshot (one member) -> copy file to all -> stop -> restore -> start
set -euo pipefail

PHASE="${1:?usage: owned-restore.sh snapshot|stop|restore|start}"
SNAPSHOT="${SNAPSHOT:-/var/backups/etcd/snapshot.db}"
MANIFESTS=/etc/kubernetes/manifests
STASH=/root/etcd-restore-stash
PKI=/etc/kubernetes/pki/etcd

running() { [[ -n "$(crictl ps --quiet --name "^$1\$")" ]]; }

case "$PHASE" in
  snapshot)
    etcdctl --endpoints=https://127.0.0.1:2379 --cacert="$PKI/ca.crt" \
      --cert="$PKI/server.crt" --key="$PKI/server.key" \
      snapshot save "$SNAPSHOT"
    ;;
  stop)
    # No API server may be running anywhere while etcd is restored.
    mkdir -p "$STASH"
    mv "$MANIFESTS/kube-apiserver.yaml" "$STASH/"
    while running kube-apiserver; do sleep 2; done
    mv "$MANIFESTS/etcd.yaml" "$STASH/"
    while running etcd; do sleep 2; done
    ;;
  restore)
    : "${MEMBER_NAME:?}" "${PEER_URL:?}" "${INITIAL_CLUSTER:?}" "${CLUSTER_TOKEN:?}"
    if running kube-apiserver || running etcd; then
      echo "refusing: kube-apiserver or etcd still running on this node" >&2
      exit 2
    fi
    etcdutl snapshot status "$SNAPSHOT" --write-out=table
    target="/var/lib/etcd-restored-$(date -u +%Y%m%dT%H%M%SZ)"
    # Same snapshot on every member. Writes a new member ID and cluster ID.
    # Bump value is the etcd v3.6 recovery guide's own worked example.
    etcdutl snapshot restore "$SNAPSHOT" \
      --name "$MEMBER_NAME" \
      --initial-cluster "$INITIAL_CLUSTER" \
      --initial-cluster-token "$CLUSTER_TOKEN" \
      --initial-advertise-peer-urls "$PEER_URL" \
      --bump-revision 1000000000 --mark-compacted \
      --data-dir "$target"
    mv /var/lib/etcd "/var/lib/etcd.pre-restore.$(date -u +%s)"
    mv "$target" /var/lib/etcd
    ;;
  start)
    mv "$STASH/etcd.yaml" "$MANIFESTS/"
    until running etcd; do sleep 2; done
    mv "$STASH/kube-apiserver.yaml" "$MANIFESTS/"
    # Kubernetes recommends restarting scheduler, controller-manager, kubelet.
    systemctl restart kubelet
    ;;
  *)
    echo "unknown phase: $PHASE" >&2
    exit 64
    ;;
esac
owned-restore.sh — the owned restore on a kubeadm-style control plane with stacked etcd. Copy the snapshot to every control-plane node, then run each phase on all of them before starting the next. RKE2, Talos and externally managed etcd have their own restore tooling; use that instead.

Volume data travels separately. Velero's CSI Snapshot Data Movement is "specifically designed to move CSI snapshot data to a backup storage location"; the built-in data mover writes to a unified repository "by default implemented by Kopia repository", and requires the node-agent. Velero's latest release as of 2026-09-13 is v1.18.2, published 2026-06-26. What decides sovereignty is where that repository lives.

yaml
apiVersion: velero.io/v1
kind: BackupStorageLocation
metadata:
  name: owned-objstore
  namespace: velero
spec:
  provider: aws # selects the S3 API, not AWS: any S3-compatible store
  objectStorage:
    bucket: cluster-dr
    prefix: prod-cluster
  credential:
    name: objstore-credentials
    key: cloud
  config:
    region: site-b
    s3Url: https://objstore.site-b.example.internal
    s3ForcePathStyle: "true"
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
  name: cluster-dr
  namespace: velero
spec:
  schedule: "*/30 * * * *" # an RPO term you set; example value only
  useOwnerReferencesInBackup: false
  template:
    storageLocation: owned-objstore
    includedNamespaces:
      - "*"
    snapshotMoveData: true # CSI snapshot data leaves the cluster
    ttl: 720h0m0s
A BackupStorageLocation on S3-compatible object storage the operator runs, and a Schedule that moves CSI snapshot data into it. The cron expression is an example, not a recommendation.

Measuring the Two Recovery Points Against Each Other

Setting a vendor's stated RPO against an estimate of your own compares a capability with a guess. The only comparable figure is the recovery point a drill achieved: one fault time applied to both paths, and the timestamp of the state each path actually brought back.

On the replication path, record the timestamp of the recovery point the test failover launched from, as the service reports it, and whether that point was clean for the fault you simulated. On the owned path, it is the newest backup that completed before the fault and the etcd snapshot the restore used. Inducing a real failover rather than a rehearsed switchover — the difference between a drill that measures and one that confirms — is the subject of IT disaster recovery testing without a maintenance window.

bash
#!/usr/bin/env bash
# achieved-rpo.sh: the recovery point an owned-path drill actually had.
# usage: achieved-rpo.sh <kube-context> <fault-time-utc> <etcd-snapshot.db>
set -euo pipefail

CONTEXT="${1:?usage: achieved-rpo.sh <context> <fault-utc> <snapshot.db>}"
FAULT="${2:?fault time, for example 2026-09-13T06:00:00Z}"
SNAPSHOT="${3:?path to the etcd snapshot the restore used}"
fault_epoch="$(date -u -d "$FAULT" +%s)"

# Newest backup that reached Completed before the fault. Its START is the
# conservative recovery point: writes after it began may be absent.
velero_start="$(kubectl --context "$CONTEXT" -n velero get backups.velero.io -o json |
  jq -r --argjson f "$fault_epoch" '
    [ .items[]
      | select(.status.phase == "Completed" and .status.completionTimestamp != null)
      | select((.status.completionTimestamp | fromdateiso8601) <= $f)
      | .status.startTimestamp ]
    | sort | last // empty')"
[[ -n "$velero_start" ]] || { echo "no Completed backup before the fault" >&2; exit 3; }

# Integrity check, then write time. The mtime must survive the copy off-node.
etcdutl snapshot status "$SNAPSHOT" --write-out=json >/dev/null
etcd_epoch="$(stat -c %Y "$SNAPSHOT")"
(( etcd_epoch <= fault_epoch )) || { echo "snapshot is newer than the fault" >&2; exit 3; }
velero_epoch="$(date -u -d "$velero_start" +%s)"

jq -n \
  --arg fault "$FAULT" \
  --arg velero "$velero_start" \
  --arg etcd "$(date -u -d "@$etcd_epoch" +%Y-%m-%dT%H:%M:%SZ)" \
  --argjson vgap "$((fault_epoch - velero_epoch))" \
  --argjson egap "$((fault_epoch - etcd_epoch))" \
  '{ fault_at: $fault,
     velero_recovery_point: $velero, velero_gap_seconds: $vgap,
     etcd_recovery_point: $etcd, etcd_gap_seconds: $egap,
     achieved_rpo_seconds: ([$vgap, $egap] | max) }'
achieved-rpo.sh — the owned path's achieved recovery point for one drill, as JSON. A backup's start time stands in for its recovery point because writes after it began may be missing. The output is RPO evidence for the drill record; on its own it is not a test of backup, restoration and recovery procedures.

Cost, Stated as a Model and Not a Measurement

No first-party cost data sits behind this section: Stribog has not operated either path for anyone. What follows is a model from published unit prices, arithmetic shown and assumptions written down.

AWS lists Elastic Disaster Recovery at $0.028 per source server per hour, and says replication "consumes additional resources on your AWS account, mostly storage (Amazon EBS), and compute (Amazon EC2)" — during ongoing replication and when drill or recovery instances launch. Modelled arithmetic, list price as read on 2026-09-13 and held flat: 8,760 hours a year at $0.028 is $245.28 per source server per year, or $1,226.40 per server over five years, before any EBS, EC2 or data transfer.

No Azure Site Recovery instance price appears here: it could not be read from Microsoft's pricing page when this was written. What the Azure-to-Azure FAQ does state is that the 31-day free period covers Site Recovery alone — "you might incur charges for Azure Storage, storage transactions, and data transfers. A recovered VM might also incur Azure compute charges." If best-effort capacity is not enough, Capacity Reservations are another line.

The owned path hides different lines. Put them in the same model:

  • Object storage for the retention you choose, plus any second site the bucket replicates to.
  • Transfer between cluster and bucket, and between sites.
  • Restore capacity: a standby cluster, or the lead time to build one — the owned counterpart of a capacity reservation.
  • Engineering time for the runbook and every drill. DRaaS does not delete this line; a test failover still needs someone to run it and read the result.

The last line is the one the comparison turns on, and why pricing an exit as a number belongs in the same spreadsheet: a replication fee is easy to model, and the cost of the dependency it creates is not.

Who Has to Be Able to Restore: The Compliance Boundary

DORA makes backup a tested capability, not a document. Under Article 12(2), financial entities "shall set up backup systems that can be activated in accordance with the backup policies and procedures", and "testing of the backup procedures and restoration and recovery procedures and methods shall be undertaken periodically." That applies whichever path you choose. On the replication path the test is a vendor-orchestrated failover; the evidence is what it produces plus your record of what came back.

Article 12(3) is narrower, and its condition matters: "When restoring backup data using own systems, financial entities shall use ICT systems that are physically and logically segregated from the source ICT system." It speaks to the owned path. A restore cluster sharing credentials, network or failure domain with the source is what it rules out; the bucket and the restore target have to sit apart from what they protect. Failover into a provider's control plane is a different arrangement, examined under DORA's ICT third-party risk requirements — the ground covered in the case against concentrated risk.

Article 12(4) reaches both: financial entities other than microenterprises "shall maintain redundant ICT capacities equipped with resources, capabilities and functions that are adequate to ensure business needs." On Azure-to-Azure Site Recovery, a best-effort capacity statement needs an explicit answer — is it adequate, or are Capacity Reservations the evidence? On the owned path the capacity is whatever you hold, and you can show it.

The Exit Ramp: Keeping the Recovery Path Portable

A recovery artefact is only as portable as whatever can read it. Replicated blocks in a vendor's staging area restore through that vendor's orchestrator; leaving means starting replication again on another service. A Velero backup in a bucket you run restores into any conformant cluster with Velero installed, on different storage, somewhere else.

etcd snapshots are less portable than they look. Kubernetes' documentation supports restoring snapshots taken from an etcd process of the same major.minor version, with patch versions free to differ — so a snapshot restores into a matching control plane, not into anything. That is the case for keeping both artefacts: etcd for faithful whole-cluster recovery, Velero for moving state somewhere new.

The paths also combine. Nothing stops DRaaS protecting a VM estate while the cluster on it keeps an owned backup in a separate bucket with Object Lock, so a replicated deletion or encryption cannot overwrite the locked versions. The owned artefact then doubles as the exit ramp from the vendor and as evidence for the concentration question a risk register eventually asks.

The Long Game

Every vendor figure in this article carries a date. Quotas, cadences and prices were read on 2026-09-13 and will move, some in your favour. What does not move is who can change them. An owned path drifts in its own way: the etcd 3.6 removal broke restore runbooks without touching a single backup, and only a drill finds that.

So the decision is less DRaaS-or-cluster than which recovery path you are willing to still operate in ten years, with the drills it needs. Buying recovery rents the parameters and the runbook. Owning it means writing and testing both, and keeping the ability to change every number above — for systems meant to last, the part worth owning.

§FAQ/Common questions

Frequently asked

What is disaster recovery in cloud computing?

It is restoring systems and data after a failure using cloud infrastructure, either bought as a managed service (DRaaS) or built on infrastructure you operate. The two differ in the unit of recovery. Services such as AWS Elastic Disaster Recovery replicate servers at block level — AWS describes an in-memory agent capturing blocks as they are written to disk — and recover them through the vendor's orchestrator. An owned Kubernetes path recovers declarative cluster state from an etcd snapshot and a Velero backup in object storage you control. The first hands you someone else's runbook and fixed parameters; the second makes you write and drill the runbook, and leaves every parameter adjustable.

How often does Azure Site Recovery create recovery points?

For Azure-to-Azure disaster recovery, Microsoft documents crash-consistent recovery points every five minutes, a setting that can't be modified. App-consistent recovery points can be created at a minimum frequency of one hour. The default replication policy retains recovery points for one day and leaves app-consistent snapshots disabled. Other Site Recovery scenarios document different parameters, so check the FAQ for the scenario you actually run.

How many servers can AWS Elastic Disaster Recovery protect?

AWS's quota table lists 300 replicating source servers per AWS account per Region, marked not adjustable, alongside 100 source servers in a single job, 500 across all jobs and 20 concurrent jobs in progress. It is not an absolute ceiling: AWS's FAQ describes recovering more than 300 servers into one target account by replicating through multiple staging accounts, at the cost of more accounts to govern.

Can I still use etcdctl snapshot restore?

Not on etcd 3.6 or later. The etcd 3.6 changelog lists etcdctl snapshot restore as removed; etcdutl snapshot restore is the surviving command. Kubernetes' documentation still describes the etcdctl form as deprecated and slated for removal, but the changelog is the authority. The restore writes new data directories and overwrites the member and cluster IDs to start a new logical cluster, and it must not run while any API server is up: stop all of them, restore every etcd member from the same snapshot, then restart.

Does DORA require backups to be tested?

Yes. Article 12(2) requires testing of the backup procedures and of restoration and recovery procedures and methods to be undertaken periodically. Article 12(3) adds that when a financial entity restores backup data using its own systems, those systems must be physically and logically segregated from the source ICT system, and Article 12(4) requires entities other than microenterprises to maintain redundant ICT capacities adequate to ensure business needs.

disaster recovery in cloud computingdisaster recovery for cloud computingdraas vs self-hosted kubernetes disaster recoveryazure site recovery crash-consistent recovery point frequencyaws elastic disaster recovery source server quotaetcdutl snapshot restore kubernetes cluster

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.