This website uses cookies

Read our Privacy policy and Terms of use for more information.

What it actually is

AWS Backup is a control plane for backups, not a storage engine. That is the sentence the documentation takes three pages to say. The data protection for each service still comes from that service's own snapshot machinery (EBS snapshots, RDS snapshots, DynamoDB backups, EFS and S3 versioned copies). What AWS Backup adds is one place where you declare when things get backed up, where the copies live, how long they stay, who can delete them, and whether you ever proved they restore.

Three objects carry the whole product. A backup plan is a policy made of rules (schedule, lifecycle, copy destinations). A backup vault is the container the copies land in, with its own encryption key, access policy and optional lock. A recovery point is one backup of one resource inside a vault. Resources join a plan through a resource assignment, by ARN or, more usefully, by tag. Tag a table backup=sl-demo and it is protected; remove the tag and it is not.

The list of supported resources is long and growing: EC2, EBS, S3, EFS, FSx (Lustre, Windows, ONTAP, OpenZFS), DynamoDB, RDS in all engines, Aurora, Aurora DSQL, DocumentDB, Neptune, Redshift and Redshift Serverless, Timestream for LiveAnalytics, Storage Gateway volumes, CloudFormation stacks, SAP HANA on EC2, VMware workloads and EKS clusters. Timestream for InfluxDB is not supported. This article builds with DynamoDB because it gives a complete backup, lock and restore loop in minutes and for pennies.

The model

The contract has four parts worth keeping in your head.

First, the first backup of a resource is a full copy and, for resource types that support it, later backups are incremental. For types that do not support incremental backups, every run stores a full copy, and your bill shows it. Check the feature availability table before you assume.

Second, backups live apart from their source. Delete the table and the recovery points stay in the vault until their lifecycle says otherwise. That separation is the point of the product.

Third, there are two backup styles. Periodic (snapshot) backups run on a cron schedule and can be kept for years. Continuous backups give point-in-time restore for a short window (35 days for S3, for example) and cannot be kept longer. A rule with EnableContinuousBackup set to true creates the continuous kind; the default is false.

Fourth, protection has two layers of immutability. Vault Lock turns a standard vault into write-once-read-many storage: nobody, including the root user, can delete recovery points or shorten their lifecycle while the lock is in force. A logically air-gapped vault goes further. It always has the lock in compliance mode, stores data in an AWS Backup service-owned account, encrypts with an AWS owned key by default, and can be shared to other accounts through AWS Resource Access Manager so you can restore from a clean account even if your main one is compromised. Its minimum retention is 7 days, and it is not available in every Region.

What AWS operates: the snapshot mechanics, the scheduling, the vault storage. What you operate: the plan, the tags, the IAM role, the KMS keys, and above all the restore test. Nobody operates that last one for you unless you configure it.

When to use it, when not to

You need

Reach for

Why

One policy across 5+ services and several accounts

AWS Backup

Plans, tag assignment, Organizations backup policies, cross-account copy

Ransomware-grade protection against a stolen admin

AWS Backup with Vault Lock or a logically air-gapped vault

Deletion is blocked even for privileged users

Compliance evidence that backups ran and restored

AWS Backup plus Audit Manager and restore testing

Daily compliance reports and test restores on a schedule

Fast, cheap rollback of a single database

The service's native feature (RDS automated backups, DynamoDB PITR)

Fewer moving parts; AWS Backup can sit on top later

Protect S3 against accidental overwrite only

S3 versioning

AWS Backup needs versioning anyway, then adds cost on top

Replicate data live to another Region

Service replication (S3 CRR, DynamoDB global tables)

Replication copies deletes and corruption too; it is not a backup

The one-line rule: use AWS Backup when the question is "who can prove we can recover, and who can stop someone deleting the proof". One warning from the docs themselves: native service backups and AWS Backup copies can overlap and double your cost. For DynamoDB specifically, point-in-time recovery and AWS Backup snapshots coexist, you pay for both, and if periodic snapshots meet your recovery needs you can switch native PITR off.

What it costs

You pay for five things: backup storage per GB-month, restores per GB, restore testing, cross-Region data transfer, and Audit Manager evaluations. Vault Lock itself has no additional charge; the storage under it is billed normally.

The worked examples on the AWS Backup pricing page show warm backup storage for EFS, S3 and EBS at $0.05 per GB-month in US East (N. Virginia), restores at $0.02 per GB in the EFS example, cross-Region transfer at $0.02 per GB for EBS and $0.04 per GB for EFS, and a restore testing example at $1.50 per recovery point tested. Logically air-gapped storage showed $0.0575 per GB-month in a Europe (Ireland) example. I could not read the DynamoDB and RDS per-GB rate tables from the page as text, so check the live page or the Pricing Calculator for those before you size anything.

The surprise is retention. A recovery point costs money every month until its lifecycle deletes it, and a locked vault removes your ability to delete early. Set retention to "Always" in a compliance-locked vault and you have bought permanent storage with no refund. Second surprise: restore testing is cheap per test but runs forever on its schedule, and restored resources bill until cleanup removes them. Third: S3 backups include every object version and each delete marker is charged as a 128 KiB object, so turn on "delete expired object delete markers" in your lifecycle rules.

Following along here costs cents. The table holds a few kilobytes, the backup is incremental from nothing, and we delete everything at the end.

The limits that bite

These come from the current AWS Backup quota page.

Per Region and account, you get 300 vaults (backup and logically air-gapped together), 300 backup plans, and 1,000,000 recovery points per vault. Those three are adjustable. Per plan, the hard limits are 100 resource assignments, 10 rules, and 5 copy actions per rule. You can run exactly one concurrent backup or copy job per resource, which matters when a snapshot window and an on-demand job collide. Tags on a recovery point are capped at 10.

Cross-Region and in-Region copy jobs are capped at 100 concurrent per account in the destination Region for fully managed types, then 5 per destination vault once that is hit. Cross-account copies of the same resource to the same destination Region cap at 30. Restore testing has its own set: 100 plans per account, 30 selections per plan, 30 ARNs per selection, and a start window between 1 and 168 hours.

Two quiet limits are in the lifecycle rules. A backup moved to cold storage must stay there at least 90 days, so DeleteAfterDays must be at least MoveToColdStorageAfterDays plus 90, and once a backup has transitioned you cannot change the transition value. And a rule's StartWindowMinutes, if you set it, must be at least 60.

Build it

We will protect a DynamoDB table, lock a vault in governance mode, take a backup, restore it into a second table, prove the data matches, prove the lock holds, and finally wire up automated restore testing. I assembled this from the current AWS Backup developer guide and API reference; run it in a sandbox account first, because I have not executed it against yours.

Prerequisites. Python 3.10+, boto3, AWS CLI v2 configured, and an IAM identity that can create IAM roles and use AWS Backup and DynamoDB. For a throwaway account the managed policies AWSBackupFullAccess, AmazonDynamoDBFullAccess and IAMFullAccess do the job. In a real account, scope them down: AWSBackupOperatorAccess lets an operator assign resources, run on-demand backups and restore, but cannot edit plans or delete scheduled backups.

Step 1 - the service role. AWS Backup assumes a role to back up and restore on your behalf. The trust policy names backup.amazonaws.com, and two managed policies carry the permissions: AWSBackupServiceRolePolicyForBackup and AWSBackupServiceRolePolicyForRestores.

import json, time, boto3

REGION = "us-east-1"
iam = boto3.client("iam")
backup = boto3.client("backup", region_name=REGION)
ddb = boto3.client("dynamodb", region_name=REGION)

ROLE = "sl-backup-demo-role"
TABLE, RESTORED = "sl-backup-demo", "sl-backup-demo-restored"
VAULT, PLAN = "sl-demo-vault", "sl-demo-plan"

trust = {"Version": "2012-10-17", "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "backup.amazonaws.com"},
    "Action": "sts:AssumeRole"}]}
role = iam.create_role(RoleName=ROLE, AssumeRolePolicyDocument=json.dumps(trust))
for p in ("AWSBackupServiceRolePolicyForBackup", "AWSBackupServiceRolePolicyForRestores"):
    iam.attach_role_policy(RoleName=ROLE,
        PolicyArn=f"arn:aws:iam::aws:policy/service-role/{p}")
ROLE_ARN = role["Role"]["Arn"]
time.sleep(10)  # let IAM propagate

Step 2 - a tagged table with data.

ddb.create_table(
    TableName=TABLE,
    AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}],
    KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}],
    BillingMode="PAY_PER_REQUEST",
    Tags=[{"Key": "backup", "Value": "sl-demo"}])
ddb.get_waiter("table_exists").wait(TableName=TABLE)
for i in range(50):
    ddb.put_item(TableName=TABLE, Item={"pk": {"S": f"item-{i}"}, "n": {"N": str(i)}})
TABLE_ARN = ddb.describe_table(TableName=TABLE)["Table"]["TableArn"]

Step 3 - a vault with a lock. Governance mode is the one you can undo, which is what you want for a tutorial. You get it by omitting ChangeableForDays. Including that parameter selects compliance mode, whose lock becomes permanent after a cooling-off period of 3 to 36,500 days. Do not experiment with compliance mode on a real account unless you mean it.

backup.create_backup_vault(BackupVaultName=VAULT)
backup.put_backup_vault_lock_configuration(
    BackupVaultName=VAULT, MinRetentionDays=1, MaxRetentionDays=35)
print(backup.describe_backup_vault(BackupVaultName=VAULT)["Locked"])  # True

The min and max retention are enforced on future jobs: a backup or copy job whose lifecycle falls outside 1 to 35 days fails. They do not touch recovery points that were in the vault before the lock.

Step 4 - the plan, and the tag-based assignment. The rule runs daily at the default 05:00 UTC, keeps backups 7 days, and targets our vault. The selection grabs anything tagged backup=sl-demo.

plan = backup.create_backup_plan(BackupPlan={
    "BackupPlanName": PLAN,
    "Rules": [{
        "RuleName": "daily",
        "TargetBackupVaultName": VAULT,
        "ScheduleExpression": "cron(0 5 ? * * *)",
        "StartWindowMinutes": 60,
        "Lifecycle": {"DeleteAfterDays": 7}}]})
PLAN_ID = plan["BackupPlanId"]
sel = backup.create_backup_selection(BackupPlanId=PLAN_ID, BackupSelection={
    "SelectionName": "tagged",
    "IamRoleArn": ROLE_ARN,
    "ListOfTags": [{"ConditionType": "STRINGEQUALS",
                    "ConditionKey": "backup", "ConditionValue": "sl-demo"}]})

Before you move on, check that DynamoDB is opted in for your Region with aws backup describe-region-settings. Accounts whose first vault was created after November 21, 2021 get the advanced DynamoDB features by default; a backup plan or on-demand job also opts you in automatically.

Step 5 - do not wait for 05:00; take a backup now. An on-demand job uses the same vault and role.

job = backup.start_backup_job(
    BackupVaultName=VAULT, ResourceArn=TABLE_ARN, IamRoleArn=ROLE_ARN,
    Lifecycle={"DeleteAfterDays": 7})
JOB_ID = job["BackupJobId"]
while True:
    d = backup.describe_backup_job(BackupJobId=JOB_ID)
    print(d["State"])
    if d["State"] in ("COMPLETED", "FAILED", "ABORTED", "EXPIRED"):
        break
    time.sleep(15)
RP_ARN = d["RecoveryPointArn"]

Step 6 - the drill: restore into a different table and compare. This is the step teams skip. Pull the metadata the recovery point supports, override only the target name, and restore. The target table must not exist; the restored table is created fresh, so nothing is overwritten.

meta = backup.get_recovery_point_restore_metadata(
    BackupVaultName=VAULT, RecoveryPointArn=RP_ARN)["RestoreMetadata"]
print(meta)  # inspect before you trust it
meta["targetTableName"] = RESTORED
rj = backup.start_restore_job(
    RecoveryPointArn=RP_ARN, IamRoleArn=ROLE_ARN,
    ResourceType="DynamoDB", Metadata=meta)
while True:
    r = backup.describe_restore_job(RestoreJobId=rj["RestoreJobId"])
    print(r["Status"])
    if r["Status"] in ("COMPLETED", "FAILED", "ABORTED"):
        break
    time.sleep(20)

def count(t):
    return ddb.scan(TableName=t, Select="COUNT")["Count"]
print(count(TABLE), count(RESTORED))  # expect 50 50

Note what a restore does not give you. The data and key schema come back. Settings that live outside the backup, such as autoscaling policies, alarms and IAM policies, you must reapply. An untested restore runbook that forgets those is how a "successful" restore turns into an outage.

Step 7 - verification: prove the lock holds. Try to delete the recovery point. In a locked vault the operation is denied for every user, root included.

from botocore.exceptions import ClientError
try:
    backup.delete_recovery_point(BackupVaultName=VAULT, RecoveryPointArn=RP_ARN)
    print("DELETED - the lock is not working")
except ClientError as e:
    print("blocked:", e.response["Error"]["Code"])

You should see a denial. I did not pin the exact error code in the docs, so the script prints whatever comes back rather than asserting one. Then confirm with aws backup describe-backup-vault --backup-vault-name sl-demo-vault, which returns Locked: true along with the retention bounds.

Step 8 - automate the drill. One manual restore proves nothing about next month. A restore testing plan does steps 5 and 6 for you on a schedule and cleans up behind itself. The plan picks recovery points from the vaults you list and restores at most one point per resource per run. Selections are per resource type, by ARN or by wildcard plus tag conditions. Restored resources are tagged awsbackup-restore-test (DynamoDB, which cannot be tagged on restore, is identified by a name prefix of awsbackup-restore-test) and deleted after the validation window.

backup.create_restore_testing_plan(RestoreTestingPlan={
    "RestoreTestingPlanName": "sl_demo_restore_test",
    "ScheduleExpression": "cron(0 6 * * ? *)",
    "ScheduleExpressionTimezone": "UTC",
    "StartWindowHours": 1,
    "RecoveryPointSelection": {
        "Algorithm": "LATEST_WITHIN_WINDOW",
        "IncludeVaults": [f"arn:aws:backup:{REGION}:{boto3.client('sts').get_caller_identity()['Account']}:backup-vault:{VAULT}"],
        "SelectionWindowDays": 7}})
backup.create_restore_testing_selection(
    RestoreTestingPlanName="sl_demo_restore_test",
    RestoreTestingSelection={
        "RestoreTestingSelectionName": "ddb_tagged",
        "ProtectedResourceType": "DynamoDB",
        "IamRoleArn": ROLE_ARN,
        "ProtectedResourceArns": [TABLE_ARN],
        "ValidationWindowHours": 1})

Restore testing can also wait for your verdict. If your pipeline runs its own checks (row counts, a smoke query), report the result with put_restore_validation_result(RestoreJobId=..., ValidationStatus="SUCCESSFUL", ValidationStatusMessage="row count matched"), where the valid statuses are FAILED, SUCCESSFUL, TIMED_OUT and VALIDATING. Validation is available through the API, not the console. The restore role needs the permissions in AWSBackupServiceRolePolicyForRestores, and access to the KMS keys on both sides.

Production notes. Copy to a second Region or account with a CopyActions entry in the rule (up to 5 per rule). For the ransomware scenario, copy into a logically air-gapped vault created with create_logically_air_gapped_backup_vault, with MinRetentionDays of at least 7. Vault-to-vault copies out of an air-gapped vault are on-demand only, and copying back into a standard vault needs a customer managed key. For S3, versioning is mandatory and EventBridge notifications must be on for continuous backups.

When it breaks

Job state FAILED with an access error. The role is missing a policy or the trust policy does not name backup.amazonaws.com. Attach both service role policies and add KMS access for the key that encrypts the source.

Job EXPIRED. The job did not start inside the start window. Widen StartWindowMinutes (60 is the floor) or stop colliding with another job on the same resource, since only one backup or copy job per resource runs at a time.

DeleteAfterDays rejected. You set cold storage and a retention shorter than transition plus 90 days. Raise retention or drop the transition.

Backup or copy job fails in a locked vault. The rule's retention is outside the vault's min and max. Lock settings reject, they do not clamp.

Restore fails because the table exists. The target name must be new. Pick a new name or delete the leftover table from your last drill.

Restore testing job fails with a start window message. AWS reports that the restore job was unable to start within the start window and suggests increasing it. Concurrent test restores are limited per resource type (for example 40 for DynamoDB, not adjustable).

S3 continuous backup quietly stops. If someone turns off bucket EventBridge notifications, AWS Backup emits no alert and no failed job. Watch for PutBucketNotificationConfiguration in CloudTrail or use the Config rule s3-event-notifications-enabled.

Compliance lock regret. After the cooling-off period, you cannot remove a compliance lock and you can delete the vault only when it is empty. Treat the ChangeableForDays parameter like a loaded gun. Also note an AWS-side edge: a closed account is suspended for 90 days and then AWS deletes the vault contents even if locked.

Cleanup

Delete in this order. The lock must go first, because recovery points cannot be deleted while it holds. Governance mode lets an authorized user remove it.

# restore testing
backup.delete_restore_testing_selection(
    RestoreTestingPlanName="sl_demo_restore_test", RestoreTestingSelectionName="ddb_tagged")
backup.delete_restore_testing_plan(RestoreTestingPlanName="sl_demo_restore_test")

# plan and assignment
backup.delete_backup_selection(BackupPlanId=PLAN_ID, SelectionId=sel["SelectionId"])
backup.delete_backup_plan(BackupPlanId=PLAN_ID)

# vault: unlock, empty, delete
backup.delete_backup_vault_lock_configuration(BackupVaultName=VAULT)
for rp in backup.list_recovery_points_by_backup_vault(BackupVaultName=VAULT)["RecoveryPoints"]:
    backup.delete_recovery_point(BackupVaultName=VAULT, RecoveryPointArn=rp["RecoveryPointArn"])
time.sleep(60)  # recovery point deletion is asynchronous
backup.delete_backup_vault(BackupVaultName=VAULT)

# tables and role
for t in (TABLE, RESTORED):
    ddb.delete_table(TableName=t)
for p in ("AWSBackupServiceRolePolicyForBackup", "AWSBackupServiceRolePolicyForRestores"):
    iam.detach_role_policy(RoleName=ROLE, PolicyArn=f"arn:aws:iam::aws:policy/service-role/{p}")
iam.delete_role(RoleName=ROLE)

Confirm with aws backup list-backup-vaults, aws backup list-backup-plans and aws backup list-restore-testing-plans, then check Cost Explorer filtered to the Backup service the next day. If the vault delete complains it is not empty, wait a minute and list recovery points again; deletion is asynchronous.

The takeaway is the same as for every backup tool. Having backups is a purchase; being able to restore is a property you have to keep re-proving. AWS Backup's value is that proof can be a schedule instead of a heroic afternoon.

Sources