Skip to content

Backup and Restore

Production is the launch-content environment (see Deployment), so its data must be recoverable after an accidental delete, a bad migration, or an overwritten file. This guide lists what is backed up, the retention, how to restore each store, and who owns it.

Dev is throwaway and is not covered here. Its database keeps one day of backups and its media bucket is unversioned by design.

What is backed up (production)

Store Mechanism Retention Recovers from
Database — Aurora cslewis-app-db-prod Automated backups + point-in-time restore (PITR) 7 days Dropped rows, a bad migration, table corruption
Media — S3 cslewis-uploads-prod (Wagtail images, generated audio) Object versioning, prod only Prior versions kept 90 days, then expired An overwritten or deleted media object
Redis (ElastiCache) Not backed up n/a Nothing. It holds cache and sessions and rebuilds itself.

Two extra safeguards on the database: deletion protection is on, so the cluster cannot be deleted by accident, and stack teardown takes a final snapshot (RemovalPolicy.SNAPSHOT).

Media versioning is set in .aws/iac/lib/storage-stack.ts (versioned: isProd) with a 90-day noncurrent-version expiry to bound storage cost. It applies to production only.

Restore the database (Aurora PITR)

PITR restores to a new cluster; it cannot overwrite the live one in place. Plan a short cutover.

  1. Pick the target time (any point within the last 7 days).
  2. Read the live cluster's network config. The cluster runs in isolated subnets and accepts only the compute security group, so a restore without these lands in the default network and is unreachable:
    SUBNET_GROUP=$(aws rds describe-db-clusters --profile cslewis --region us-east-2 \
      --db-cluster-identifier cslewis-app-db-prod --query "DBClusters[0].DBSubnetGroup" --output text)
    SG=$(aws rds describe-db-clusters --profile cslewis --region us-east-2 \
      --db-cluster-identifier cslewis-app-db-prod --query "DBClusters[0].VpcSecurityGroups[0].VpcSecurityGroupId" --output text)
    
  3. Restore to a new cluster in that same network:
    aws rds restore-db-cluster-to-point-in-time \
      --profile cslewis --region us-east-2 \
      --source-db-cluster-identifier cslewis-app-db-prod \
      --db-cluster-identifier cslewis-app-db-prod-restore \
      --restore-to-time <ISO-8601-timestamp> \
      --db-subnet-group-name "$SUBNET_GROUP" \
      --vpc-security-group-ids "$SG"
    
  4. Add a Serverless v2 instance to the restored cluster, then verify the data.
  5. Cut over: update DATABASE_URL in the cslewis-app-prod secret to the restored endpoint, then force a new ECS deployment. A task reads the secret only at start, so a secret change alone leaves live tasks on the old cluster. For a partial restore, export the needed rows from the restored cluster into the live one instead.
  6. Delete the temporary cluster once the restore is confirmed.

Restore a media object (S3 version)

A deleted object leaves a delete marker; an overwrite leaves the old bytes as a prior version. Both are recoverable while the version is inside the 90-day window.

  1. List the versions of the key:
    aws s3api list-object-versions \
      --profile cslewis --region us-east-2 \
      --bucket cslewis-uploads-prod --prefix <object-key>
    
  2. Recover the content:
  3. Overwritten: copy the prior VersionId back over the current key.
    aws s3api copy-object --profile cslewis --region us-east-2 \
      --bucket cslewis-uploads-prod \
      --copy-source "cslewis-uploads-prod/<object-key>?versionId=<old-version-id>" \
      --key <object-key>
    
  4. Deleted: delete the delete marker to bring the object back.
    aws s3api delete-object --profile cslewis --region us-east-2 \
      --bucket cslewis-uploads-prod --key <object-key> \
      --version-id <delete-marker-version-id>
    
  5. Invalidate the CloudFront path. Media is served through CloudFront (media-cdn-prod, distribution E2D6DSI04HRA0T) with a caching-optimized policy, so restoring the S3 object does not refresh the edge; the stale copy serves until the cache expires.
    aws cloudfront create-invalidation --profile cslewis \
      --distribution-id E2D6DSI04HRA0T --paths "/<object-key>"
    
    If the id changes, read it with aws cloudfront list-distributions --query "DistributionList.Items[?contains(Comment,'cslewis-app-media-prod')].Id".

Ownership

The backend / DevOps owner runs these restores and owns the runbook. Raise a restore request in the team channel with the store, the object or time range, and the reason. Run a restore drill before public launch (tracked on the Launch Checklist).