· nervico-team · cloud-architecture · 10 min read
DR and Backup on AWS: A Practical Strategy for Startups
Practical disaster recovery and backup strategy on AWS for startups: protection levels, real RPO/RTO, Terraform automation, and costs by scenario.
Startups do not go bankrupt because they lack a disaster recovery plan. They go bankrupt because they assumed they would never need one. And when the database gets corrupted, an employee deletes the production bucket, or an AWS region suffers a 4-hour outage, they discover they have no way to recover the service.
According to a Zetta study, 40% of companies that permanently lose their data close within the following 6 months. And it is not just about natural disasters. The most common cause of data loss in the cloud is human error: a DROP TABLE on the wrong database, a terraform destroy without confirming the workspace, or a software update that corrupts data.
This article presents a progressive DR and backup strategy for startups on AWS: starting with the essentials on a minimal budget and scaling as business criticality grows.
Fundamental Concepts
RPO and RTO: The Two Metrics That Matter
RPO (Recovery Point Objective): How much data you can afford to lose. If your RPO is 1 hour, you need backups at least every hour. If it is 0, you need real-time replication.
RTO (Recovery Time Objective): How long you can be without service. If your RTO is 4 hours, you need to be able to restore the complete service in under 4 hours. If it is 0, you need active infrastructure in another region.
RPO <---- Acceptable data loss ----> Moment of disaster
|
RTO <---> Service restored| Business Level | Typical RPO | Typical RTO | Example |
|---|---|---|---|
| Blog/marketing | 24 hours | 24 hours | Corporate website |
| Early-stage SaaS | 1-4 hours | 4-8 hours | MVP with first customers |
| Growing SaaS | 15-60 minutes | 1-4 hours | Product with revenue |
| Enterprise SaaS | 1-5 minutes | 15-60 minutes | Customers with contractual SLA |
| Finance/healthcare | Seconds | Minutes | Regulated data |
The 4 DR Strategies on AWS
AWS defines four DR strategies, ordered from lowest to highest cost and from slowest to fastest recovery:
1. Backup and Restore (low cost, high RTO):
Regular backups stored in another region. In case of disaster, you restore infrastructure and data from backups. RTO: hours. RPO: depends on backup frequency.
2. Pilot Light (low-medium cost, medium RTO):
Essential infrastructure (replicated database, configuration) is always active in the secondary region. In case of disaster, you start the remaining infrastructure (compute, load balancers). RTO: 30-60 minutes.
3. Warm Standby (medium cost, low RTO):
A reduced version of the complete infrastructure is always active in the secondary region. In case of disaster, you scale to production size. RTO: 10-30 minutes.
4. Active-Active (high cost, minimal RTO):
Complete infrastructure in two or more regions, serving traffic simultaneously. In case of disaster, traffic redirects automatically. RTO: seconds.
Strategy for Startups: A Progressive Approach
Level 1: The Bare Minimum ($0-50/Month)
If you are in pre-revenue or early-stage, you do not need active-active multi-region. But you do need to protect against the most common causes of data loss: human error and corruption.
Automated RDS Backups:
RDS includes automated backups at no additional cost (beyond storage):
# Verify that automated backups are active
aws rds describe-db-instances \
--db-instance-identifier my-database \
--query 'DBInstances[0].{BackupRetention:BackupRetentionPeriod,BackupWindow:PreferredBackupWindow}'Configure retention to a minimum of 7 days (the maximum is 35). Backups are stored in S3 managed by AWS and allow restoring to any point in time (PITR) within the retention period.
# Terraform: RDS with backup configured
resource "aws_db_instance" "production" {
identifier = "production-db"
engine = "postgres"
engine_version = "16.1"
instance_class = "db.t4g.medium"
allocated_storage = 50
storage_encrypted = true
backup_retention_period = 14 # 14 days retention
backup_window = "03:00-04:00" # Backup window (UTC)
maintenance_window = "Mon:04:00-Mon:05:00"
deletion_protection = true # Prevent accidental deletion
skip_final_snapshot = false
final_snapshot_identifier = "production-db-final"
}S3 Versioning:
S3 Versioning maintains all versions of each object. If someone overwrites or deletes a file, you can restore the previous version.
resource "aws_s3_bucket_versioning" "production" {
bucket = aws_s3_bucket.production.id
versioning_configuration {
status = "Enabled"
}
}
# Lifecycle rule: move old versions to Glacier after 30 days
resource "aws_s3_bucket_lifecycle_configuration" "production" {
bucket = aws_s3_bucket.production.id
rule {
id = "archive-old-versions"
status = "Enabled"
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "GLACIER"
}
noncurrent_version_expiration {
noncurrent_days = 365
}
}
}MFA Delete for Extra Protection:
Enable MFA Delete on critical buckets. Requires MFA authentication to delete versions or disable versioning.
AWS Backup to Centralize:
AWS Backup allows managing all backups from a central point:
# Centralized backup plan
resource "aws_backup_plan" "daily" {
name = "daily-backup-plan"
rule {
rule_name = "daily-backup"
target_vault_name = aws_backup_vault.production.name
schedule = "cron(0 3 * * ? *)" # Daily at 3:00 UTC
lifecycle {
delete_after = 30 # Retain for 30 days
}
copy_action {
destination_vault_arn = aws_backup_vault.dr_region.arn
lifecycle {
delete_after = 14 # Retain copies 14 days in DR region
}
}
}
}
# Resource selection for protection
resource "aws_backup_selection" "all_critical" {
iam_role_arn = aws_iam_role.backup_role.arn
name = "critical-resources"
plan_id = aws_backup_plan.daily.id
selection_tag {
type = "STRINGEQUALS"
key = "Backup"
value = "true"
}
}Level 1 Cost: With a 50 GB database and 100 GB in S3, the additional backup cost is under $20/month.
Level 2: Cross-Region Protection ($50-200/Month)
When you have paying customers and an implicit (or explicit) SLA, you need to protect against losing a complete AWS region. It happens rarely, but regional outages of 1-4 hours have occurred multiple times in AWS history.
Cross-Region RDS Replication:
# Cross-region Read Replica (serves as DR base)
resource "aws_db_instance" "dr_replica" {
provider = aws.dr_region
replicate_source_db = aws_db_instance.production.arn
instance_class = "db.t4g.medium"
storage_encrypted = true
kms_key_id = aws_kms_key.dr_rds.arn
publicly_accessible = false
vpc_security_group_ids = [aws_security_group.dr_rds.id]
db_subnet_group_name = aws_db_subnet_group.dr.name
tags = {
Name = "dr-replica"
Role = "disaster-recovery"
}
}The cross-region replica has a replication lag of seconds to minutes. In case of disaster, you promote the replica to primary:
# Promote replica to primary (in the DR region)
aws rds promote-read-replica \
--db-instance-identifier dr-replica \
--region eu-central-1Cross-Region S3 Replication:
resource "aws_s3_bucket_replication_configuration" "production" {
bucket = aws_s3_bucket.production.id
role = aws_iam_role.replication.arn
rule {
id = "replicate-all"
status = "Enabled"
destination {
bucket = aws_s3_bucket.dr.arn
storage_class = "STANDARD_IA"
}
}
}Cross-Region EBS Snapshots:
For EBS volumes (EC2), automate snapshot copying to the DR region:
import boto3
ec2_source = boto3.client('ec2', region_name='eu-west-1')
ec2_dr = boto3.client('ec2', region_name='eu-central-1')
def copy_latest_snapshots():
# Find recent snapshots
snapshots = ec2_source.describe_snapshots(
Filters=[
{'Name': 'tag:Backup', 'Values': ['true']},
{'Name': 'status', 'Values': ['completed']}
],
OwnerIds=['self']
)
for snap in snapshots['Snapshots']:
# Copy to DR region
ec2_dr.copy_snapshot(
SourceRegion='eu-west-1',
SourceSnapshotId=snap['SnapshotId'],
Description=f"DR copy of {snap['SnapshotId']}",
Encrypted=True,
KmsKeyId='alias/dr-ebs-key'
)Level 3: Pilot Light Multi-Region ($200-500/Month)
For startups with significant revenue and contractual SLAs with customers.
Infrastructure in DR Region:
# VPC in DR region (always active)
module "dr_vpc" {
source = "./modules/vpc"
providers = { aws = aws.dr_region }
cidr_block = "10.1.0.0/16"
name = "dr-vpc"
}
# Cross-region RDS replica (always active)
# Already configured in Level 2
# ECS cluster (defined but without active tasks)
resource "aws_ecs_cluster" "dr" {
provider = aws.dr_region
name = "dr-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
# ALB (defined but without active targets)
resource "aws_lb" "dr" {
provider = aws.dr_region
name = "dr-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.dr_alb.id]
subnets = module.dr_vpc.public_subnet_ids
}Failover with Route 53 Health Checks:
# Primary service health check
resource "aws_route53_health_check" "primary" {
fqdn = "api.yourdomain.com"
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
tags = {
Name = "primary-health-check"
}
}
# DNS failover
resource "aws_route53_record" "api" {
zone_id = aws_route53_zone.main.zone_id
name = "api.yourdomain.com"
type = "A"
alias {
name = aws_lb.primary.dns_name
zone_id = aws_lb.primary.zone_id
evaluate_target_health = true
}
set_identifier = "primary"
failover_routing_policy {
type = "PRIMARY"
}
health_check_id = aws_route53_health_check.primary.id
}
resource "aws_route53_record" "api_dr" {
zone_id = aws_route53_zone.main.zone_id
name = "api.yourdomain.com"
type = "A"
alias {
name = aws_lb.dr.dns_name
zone_id = aws_lb.dr.zone_id
evaluate_target_health = false
}
set_identifier = "secondary"
failover_routing_policy {
type = "SECONDARY"
}
}DR Activation Runbook:
The pilot light needs a documented and tested runbook:
#!/bin/bash
# runbook-activate-dr.sh
# Execute when the primary region is unavailable
echo "1. Promoting database replica..."
aws rds promote-read-replica \
--db-instance-identifier dr-replica \
--region eu-central-1
echo "2. Scaling ECS services in DR region..."
aws ecs update-service \
--cluster dr-cluster \
--service api-service \
--desired-count 3 \
--region eu-central-1
echo "3. Verifying DR ALB health check..."
aws elbv2 describe-target-health \
--target-group-arn arn:aws:elasticloadbalancing:eu-central-1:123456789:targetgroup/dr-api/abc \
--region eu-central-1
echo "4. Route 53 should failover automatically."
echo " Verifying DNS resolution..."
dig api.yourdomain.com +short
echo "DR activated. Monitor for the next 24 hours."Backup Automation: What Is Not Automated Does Not Get Done
AWS Backup with Notifications
# SNS topic for backup alerts
resource "aws_sns_topic" "backup_alerts" {
name = "backup-failure-alerts"
}
resource "aws_sns_topic_subscription" "email" {
topic_arn = aws_sns_topic.backup_alerts.arn
protocol = "email"
endpoint = "[email protected]"
}
# EventBridge rule for backup failures
resource "aws_cloudwatch_event_rule" "backup_failure" {
name = "backup-job-failure"
event_pattern = jsonencode({
source = ["aws.backup"]
detail-type = ["Backup Job State Change"]
detail = {
state = ["FAILED", "EXPIRED"]
}
})
}
resource "aws_cloudwatch_event_target" "sns" {
rule = aws_cloudwatch_event_rule.backup_failure.name
target_id = "send-to-sns"
arn = aws_sns_topic.backup_alerts.arn
}Periodic Restore Verification
A backup that has not been tested is not a backup. It is a hope. Automate monthly restore tests:
import boto3
from datetime import datetime
rds = boto3.client('rds', region_name='eu-west-1')
def test_restore():
timestamp = datetime.utcnow().strftime('%Y%m%d%H%M')
test_instance = f"restore-test-{timestamp}"
# Restore from the latest snapshot
snapshots = rds.describe_db_snapshots(
DBInstanceIdentifier='production-db',
SnapshotType='automated'
)
latest = sorted(
snapshots['DBSnapshots'],
key=lambda x: x['SnapshotCreateTime'],
reverse=True
)[0]
print(f"Restoring from snapshot: {latest['DBSnapshotIdentifier']}")
rds.restore_db_instance_from_db_snapshot(
DBInstanceIdentifier=test_instance,
DBSnapshotIdentifier=latest['DBSnapshotIdentifier'],
DBInstanceClass='db.t4g.micro',
PubliclyAccessible=False
)
# Wait for availability, run verifications, and delete
print(f"Test instance created: {test_instance}")
print("Verify manually and delete with:")
print(f" aws rds delete-db-instance --db-instance-identifier {test_instance} --skip-final-snapshot")Real Costs by Protection Level
| Level | Strategy | RPO | RTO | Additional Cost/Month |
|---|---|---|---|---|
| 1 | Backup and Restore (same region) | 1-24 hours | 4-24 hours | $10-50 |
| 2 | Cross-region backup | 1-4 hours | 2-8 hours | $50-200 |
| 3 | Pilot Light | 5-60 minutes | 30-60 minutes | $200-500 |
| 4 | Warm Standby | 1-5 minutes | 10-30 minutes | $500-2,000 |
| 5 | Active-Active | Seconds | Seconds | $2,000-10,000 |
Common Mistakes
Mistake 1: Relying Solely on Automated RDS Backups
Automated RDS backups are deleted when you delete the instance. If someone runs terraform destroy by mistake or manually deletes the instance, the backups disappear. Solution: create additional manual snapshots and copy them to another region.
Mistake 2: Not Protecting Against Accidental Deletion
The primary cause of data loss in the cloud is not a catastrophe. It is a human with excessive permissions. Enable:
deletion_protectionon RDS- S3 Versioning + MFA Delete
prevent_destroyin Terraform for critical resources- Restrictive IAM policies (no AdministratorAccess for developers)
Mistake 3: Not Testing Restoration
We have seen companies with perfectly configured backups that discovered, during an actual incident, that restoration failed because the format had changed, permissions were incorrect, or the process was not documented. Test restoration at least once per quarter.
Mistake 4: Unencrypted Backups
If your production data is encrypted (and it should be), backups must be encrypted with the same policy. An unencrypted backup in S3 is a vulnerability, especially if regulations require data protection (GDPR, HIPAA).
Mistake 5: Not Considering Data Outside AWS
If your application generates data stored in external services (Stripe, Firebase, Algolia, managed Elasticsearch), that data needs its own backup strategy. Do not assume the SaaS provider maintains accessible backups.
DR Checklist for Startups
Immediate (week 1):
- Automated RDS backups enabled with 14-day retention
- S3 Versioning enabled on production buckets
- Deletion protection on RDS and critical buckets
- Backup failure alerts configured
Short term (month 1):
- AWS Backup configured for all critical resources
- Backup copies to a second region
- Documented restoration runbook
- First restore test completed
Medium term (quarter 1):
- Cross-region RDS replica for critical data
- Cross-region S3 replication
- Route 53 health checks configured
- Automated quarterly restore test
Long term (as growth demands):
- Pilot light or warm standby in secondary region
- Automated failover with Route 53
- Simulated DR exercises (game days) twice a year
- Documented SLAs with customers
Conclusion
Disaster recovery is not a project. It is a continuous practice that scales with your business. A pre-revenue startup needs automated backups and accidental deletion protection. A startup with enterprise customers needs cross-region replication and automated failover.
The most dangerous mistake is doing nothing. The second most dangerous is configuring backups and never testing restoration. The cost of a basic DR strategy (Level 1-2) is under $200 per month. The cost of losing your customers’ data is the entire company.
If you need help designing a DR strategy appropriate to your growth stage, our AWS consulting team implements these strategies regularly. Request a free audit to evaluate your current level of protection.