Kubernetes Disaster Recovery: Cluster Backup, etcd Snapshots, and Velero
A complete DR strategy has three layers — GitOps (cluster config in Git), Velero (PVC data backup), and infrastructure-as-code (cluster rebuild automation) —...
Quick Answer: A complete DR strategy has three layers — GitOps (cluster config in Git), Velero (PVC data backup), and infrastructure-as-code (cluster rebuild automation) — with regular tested restore drills to verify it actually works.
Detailed Answer:
DR Layers:
Layer 1: Configuration Recovery (GitOps)
→ All K8s manifests in Git → can redeploy to new cluster in minutes
→ ArgoCD/Flux re-syncs entire cluster state from Git automatically
→ RTO: ~15 minutes for stateless apps
→ RPO: Last Git commit (effectively zero for config)
Layer 2: Data Recovery (Velero + AWS Backup)
→ PVC snapshots via Velero → stored in S3
→ EBS snapshots via AWS Backup → point-in-time recovery
→ RTO: 30-60 minutes (restore + verify)
→ RPO: Last backup (typically 1-6 hours)
Layer 3: Infrastructure Recovery (Terraform)
→ EKS cluster, VPC, node groups defined in Terraform
→ Can rebuild entire infrastructure from scratch
→ RTO: 20-30 minutes for cluster provisioning
→ RPO: N/A (infrastructure is stateless config)
Velero Setup and Configuration:
# Install Velero with AWS S3 backend
# First create S3 bucket and IAM role for Velero
# IAM policy for Velero (attach to IRSA role)
aws iam create-policy \
--policy-name VeleroPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ec2:DescribeVolumes",
"ec2:DescribeSnapshots",
"ec2:CreateTags",
"ec2:CreateVolume",
"ec2:CreateSnapshot",
"ec2:DeleteSnapshot"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:DeleteObject",
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": "arn:aws:s3:::my-velero-backups/*"
},
{
"Effect": "Allow",
"Action": ["s3:ListBucket"],
"Resource": "arn:aws:s3:::my-velero-backups"
}
]
}'
# Install Velero via Helm with IRSA
helm install velero vmware-tanzu/velero \
--namespace velero \
--create-namespace \
--set configuration.provider=aws \
--set configuration.backupStorageLocation.bucket=my-velero-backups \
--set configuration.backupStorageLocation.config.region=us-east-1 \
--set configuration.volumeSnapshotLocation.config.region=us-east-1 \
--set serviceAccount.server.annotations."eks\.amazonaws\.com/role-arn"=\
arn:aws:iam::123456789012:role/VeleroRole \
--set initContainers[0].name=velero-plugin-for-aws \
--set initContainers[0].image=velero/velero-plugin-for-aws:v1.9.0 \
--set initContainers[0].volumeMounts[0].mountPath=/target \
--set initContainers[0].volumeMounts[0].name=plugins
Backup Schedules:
# Schedule 1: Full cluster backup every 6 hours
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: cluster-backup-6h
namespace: velero
spec:
schedule: "0 */6 * * *"
template:
includedNamespaces:
- "*"
excludedNamespaces:
- velero
- kube-system # Usually no need to back up system namespace
includeClusterResources: true # PVs, StorageClasses, ClusterRoles
snapshotVolumes: true # Snapshot EBS volumes
storageLocation: default
volumeSnapshotLocations:
- default
ttl: 720h # Retain for 30 days
labelSelector:
matchExpressions:
- key: backup-exclude
operator: DoesNotExist # Skip pods labeled backup-exclude=true
---
# Schedule 2: Critical namespace backup every hour
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: production-backup-1h
namespace: velero
spec:
schedule: "0 * * * *"
template:
includedNamespaces:
- production
snapshotVolumes: true
storageLocation: default
ttl: 168h # Retain for 7 days
Backup and Restore Operations:
# Manual backup before risky operation
velero backup create pre-upgrade-backup \
--include-namespaces production \
--snapshot-volumes \
--wait
# Check backup status
velero backup describe pre-upgrade-backup
velero backup logs pre-upgrade-backup
# List all backups
velero backup get
# Restore entire namespace
velero restore create \
--from-backup pre-upgrade-backup \
--include-namespaces production \
--wait
# Restore specific resources only
velero restore create \
--from-backup pre-upgrade-backup \
--include-resources deployments,services \
--namespace-mappings production:production-restored
# Check restore status
velero restore describe <restore-name>
velero restore logs <restore-name>
Multi-Region DR with Cross-Region Replication:
# Terraform — S3 bucket with cross-region replication for Velero backups
resource "aws_s3_bucket" "velero_primary" {
bucket = "my-velero-backups-us-east-1"
provider = aws.us_east_1
}
resource "aws_s3_bucket" "velero_dr" {
bucket = "my-velero-backups-us-west-2"
provider = aws.us_west_2
}
resource "aws_s3_bucket_replication_configuration" "velero" {
provider = aws.us_east_1
bucket = aws_s3_bucket.velero_primary.id
role = aws_iam_role.replication.arn
rule {
id = "replicate-to-dr-region"
status = "Enabled"
destination {
bucket = aws_s3_bucket.velero_dr.arn
storage_class = "STANDARD_IA" # Cheaper for DR storage
}
}
}
DR Runbook — Complete Cluster Recovery:
#!/bin/bash
# DR Runbook: Restore production cluster after catastrophic failure
echo "=== STEP 1: Provision new EKS cluster ==="
cd terraform/eks
terraform init
terraform apply -var="cluster_name=production-dr" \
-var="region=us-west-2" \
-auto-approve
echo "=== STEP 2: Configure kubectl ==="
aws eks update-kubeconfig \
--name production-dr \
--region us-west-2
echo "=== STEP 3: Install Velero ==="
helm install velero vmware-tanzu/velero \
--namespace velero \
--create-namespace \
--values velero-values.yaml
echo "=== STEP 4: Connect to backup storage ==="
velero backup-location create default \
--provider aws \
--bucket my-velero-backups-us-west-2 \
--config region=us-west-2
echo "=== STEP 5: Wait for backup sync ==="
velero backup get # Wait until backups appear
echo "=== STEP 6: Restore from latest backup ==="
LATEST_BACKUP=$(velero backup get \
--output json | \
jq -r '.items | sort_by(.metadata.creationTimestamp) | last | .metadata.name')
velero restore create dr-restore \
--from-backup $LATEST_BACKUP \
--wait
echo "=== STEP 7: Verify restoration ==="
kubectl get pods -A
kubectl get pvc -A
echo "=== STEP 8: Bootstrap ArgoCD ==="
kubectl apply -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml -n argocd
# ArgoCD auto-syncs remaining config from Git
echo "=== STEP 9: Update DNS ==="
# Point your domain to new cluster's load balancer
NEW_LB=$(kubectl get svc -n ingress-nginx \
-o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}')
echo "Update DNS to point to: $NEW_LB"
echo "=== DR Recovery Complete ==="
DR Testing — Monthly Drill:
# Test restore without affecting production
# Use a separate namespace to validate backup integrity
velero restore create test-restore-$(date +%Y%m%d) \
--from-backup production-backup-latest \
--namespace-mappings production:dr-test \
--include-namespaces production
# Verify data integrity
kubectl exec -it postgres-0 -n dr-test -- \
psql -U postgres -c "SELECT COUNT(*) FROM orders;"
# Compare with production count
kubectl exec -it postgres-0 -n production -- \
psql -U postgres -c "SELECT COUNT(*) FROM orders;"
# Clean up test restore
kubectl delete namespace dr-test
DR Checklist:
| Item | Frequency | Tool | Owner | |---|---|---|---| | Full cluster backup | Every 6 hours | Velero | Automated | | Production namespace backup | Every 1 hour | Velero | Automated | | EBS snapshots | Every 6 hours | AWS Backup | Automated | | Backup integrity check | Daily | Velero describe | Automated | | Full restore drill | Monthly | Velero restore | Ops team | | Terraform plan (drift check) | Weekly | Terraform plan | Automated | | RTO/RPO verification | Quarterly | Restore drill | Ops team | | DR runbook review | Quarterly | Manual | Ops team |
Key Takeaway: A DR strategy you've never tested is not a DR strategy — schedule monthly restore drills into a separate namespace or cluster to verify your backups are valid and your team knows the runbook.
Finished reading?
Mark as complete to claim your +20 XP and track progress.
