AWS Backup · 2026-11-08 · Backup arc
AWS Backup for MCP Servers: Vaults, Cross-Account DR, Restore Testing, and Compliance
AliveMCP monitors thousands of MCP endpoints every 60 seconds. Among the AWS-deployed servers we watch, the incidents that cause the longest recovery windows fall into two distinct categories: servers that went down and recovered automatically within minutes, and servers that went down and whose operators discovered their backup strategy was either incomplete or had never been validated. The second category is rare — but when it happens, it is catastrophic. The MCP server is stateful. It stores tool schemas, session state, tenant configuration, and often embeddings or cached computation results in DynamoDB tables and EFS volumes. These are not cache layers that can be rebuilt from primary sources in minutes. They are primary sources themselves. Losing them permanently, or losing access to them for hours during an account compromise incident or region outage, is not a performance issue — it is a business-ending event for a product that sells on reliability. The uncomfortable truth that AliveMCP's monitoring data reveals is that most MCP server operators spend significant effort on uptime monitoring and deployment automation, and comparatively little on the data protection layer underneath. They create a DynamoDB table, they enable point-in-time recovery in a CDK stack, and they consider the backup problem solved. They do not test whether that PITR backup can actually be restored to a working table. They do not copy recovery points to a separate account where an IAM credential compromise cannot delete them. They do not have documented compliance evidence showing that every new table gets a backup plan applied within 24 hours of creation. AWS Backup is the managed control plane that makes all four of those gaps addressable with a single coherent infrastructure layer. This guide synthesizes four deep-dives — backup vaults, plans, and resource assignment, cross-account copy and Organizations disaster recovery, automated restore testing with Lambda validation, and Audit Manager compliance controls for SOC 2 and HIPAA — into a complete AWS Backup strategy for MCP server operators.
TL;DR
- Backup vaults, plans, and resource assignment — create one backup vault per environment with a customer-managed KMS key; create a backup plan with two rules: daily (retention 35 days, move-to-cold-storage after 7 days, completion window 4 hours) and weekly (retention 365 days, move-to-cold after 30 days, completion window 8 hours); assign resources by tag
Backup=trueso any new DynamoDB table, EFS file system, or RDS cluster with that tag is automatically protected from its first backup window; enable DynamoDB PITR independently on every table because AWS Backup's scheduled snapshots and PITR are complementary and non-overlapping — PITR gives you second-granularity recovery for the last 35 days, scheduled snapshots extend retention to 365 days; enable vault lock in governance mode during setup and transition to compliance mode (which is permanent after the cool-off period) before your first SOC 2 audit: The tag-based resource assignment pattern is the single most important operational decision in your backup strategy. Explicit ARN assignments are fragile — they require updating every time you provision a new resource, and a resource that is not in the ARN list is silently unprotected. Tag-based assignment means your backup plan automatically picks up every new DynamoDB table or EFS file system that has theBackup=truetag applied at creation. This shifts the error mode from "forgot to add to backup plan" (silent, no alert) to "forgot to add the tag" (visible in Audit Manager's BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN control, which will flag the untagged resource as non-compliant). The tag approach also works with AWS Organizations backup policies — the sameBackup=truetag selector used in your account-level backup plan works identically in an org-level policy, so you can enforce consistent backup coverage across every member account without touching each account's IaC stack individually. - Cross-account copy and Organizations disaster recovery — a backup vault in the same account as your production data provides no protection against account compromise, accidental bulk deletion, or an IAM role with
backup:DeleteRecoveryPointthat a compromised credential can call; configure a copy action in your daily backup plan rule that sends every new recovery point to a destination vault in a separate DR account; the prerequisites are three one-time setup commands from the management account (enable trusted access withaws organizations enable-aws-service-access --service-principal backup.amazonaws.com, enable cross-account backup withaws backup update-global-settings --global-settings '{"isCrossAccountBackupEnabled":"true"}', and register the DR account as delegated admin), plus two resources in the DR account (a KMS key whose policy grantskms:Decrypt,kms:GenerateDataKey, andkms:DescribeKeytobackup.amazonaws.comwith anaws:SourceAccountcondition scoped to the production account ID, and a vault whose access policy grantsbackup:CopyIntoBackupVaultto the production account root); the CDK L2 BackupPlanRule does not expose acopyActionsproperty directly — use the CloudFormation escape hatch to add theCopyActionsarray to the rule; monitor copy job status via EventBridgeCopy Job State Changeevents from theaws.backupsource: The critical architecture insight for cross-account backup is that the DR account must have its ownisCrossAccountBackupEnabled=trueglobal setting, not just the source account. Many teams enable the setting in production, see the copy actions configured correctly, then hitCrossAccountBackupNotEnablederrors at runtime because they forgot the DR account also needs the global setting enabled. The other common mistake is using the AWS-managed key (aws/backup) in the DR vault — it does not support the cross-account trust condition needed for the backup service principal from a foreign account. - Automated restore testing — a backup that has never been tested is an assumption, not a guarantee; AWS Backup Restore Testing automates the full recovery validation cycle: select a recovery point (the most recent within the last 24 hours using
LATEST_WITHIN_WINDOW), restore it to a temporary resource, invoke a validation Lambda that checks data integrity, wait for the Lambda to reportSUCCESSFULorFAILEDviabackup:PutRestoreValidationResult, then clean up the temporary resource automatically; create the plan on a weekly schedule (Sunday 06:00 UTC) with a 1-hour start window and a 4-hour validation window (not 1 hour — DynamoDB table restore takes 10–30 minutes, then Lambda cold start, then validation logic execution); the validation Lambda subscribes to the EventBridgeRestore Testing Job State Changeevent withstatus: COMPLETED, queries the restored table'sItemCountfromDescribeTable, compares it against the expected count stored in SSM Parameter Store with a 10% tolerance window, performs a test write-and-delete to verify the table is writable, then callsPutRestoreValidationResultwithSUCCESSFULorFAILED; keep the expected row count in SSM up to date with a nightly Lambda that callsDescribeTableand writes theItemCountto/alivemcp/sessions-table-row-count: The validation window length is the most common misconfiguration in restore testing setups. Teams setvalidationWindowHours: 1thinking that one hour is plenty, then find their tests consistently reportTIMED_OUTbecause they did not account for the DynamoDB restore time (10–30 minutes depending on table size and secondary index count), the Lambda cold start (a few hundred milliseconds to a few seconds), and the EventBridge event delivery latency between restore completion and Lambda invocation. Multiply by retry backoff if the Lambda has an IAM permissions error on first invocation. Setting 4 hours is the safe default; set it back to 2 if your tables are small and your confidence in the IAM setup is high. - Audit Manager compliance — AWS Backup Audit Manager continuously evaluates your backup coverage against five key controls:
BACKUP_PLAN_MIN_FREQUENCY_AND_MIN_RETENTION_CHECK(daily frequency, 35-day minimum retention, scoped toBackup=truetagged resources),BACKUP_RECOVERY_POINT_ENCRYPTED(all recovery points in DynamoDB, EFS, RDS, and EBS vaults encrypted — requires a customer-managed KMS key on the vault, not the AWS-managedaws/backupkey),BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN(every DynamoDB, EFS, and RDS resource withBackup=trueis assigned to at least one plan),BACKUP_RECOVERY_POINT_MINIMUM_RETENTION_CHECK(35-day minimum retention on all recovery points), andBACKUP_LAST_RECOVERY_POINT_CREATED(at least one recovery point created within 26 hours for each protected resource — set 26 not 24 to avoid false negatives when backup jobs complete at hour 25); create a report plan that exports daily compliance reports to S3 in both CSV and JSON format; for SOC 2 Type II, attach theRESOURCE_COMPLIANCE_REPORTexports for each month of the audit period — auditors look for consistentCOMPLIANTstatus across all reporting months: The two services named "Audit Manager" are separate: AWS Backup Audit Manager (covered here, focused exclusively on backup compliance controls) and AWS Audit Manager (a broader compliance assessment service that gathers evidence from many services). For backup compliance specifically, AWS Backup Audit Manager is the right choice because its built-in controls understand backup-specific concepts — recovery point metadata, vault encryption status, backup job frequency per resource — that AWS Audit Manager's generic evidence collection does not model directly. The Audit Manager framework also has a natural connection to your AWS Config rules, since each control maps to a managed Config rule, which means you can attach SSM Automation remediation to automatically apply theBackup=truetag to any resource flagged as unprotected.
Part 1: Backup vault and plan fundamentals
The first question every MCP server operator should answer about their backup strategy is not "how often do we back up?" but "where do our recovery points live and can they be deleted?" AWS Backup's core building block — the backup vault — answers the second question. A vault is an encrypted container for recovery points. Recovery points can only be deleted by explicitly deleting from the vault, not by deleting the source resource. If someone accidentally deletes the DynamoDB table, the recovery points in the vault are unaffected. If vault lock is enabled in compliance mode, not even the root account user can delete recovery points before their retention period expires.
Create separate vaults for dev and prod. A runaway cleanup script in dev cannot touch prod backups, and you can apply different retention and lock configurations to each environment without complicating a single shared vault. Use a customer-managed KMS key for each vault — the AWS-managed aws/backup key works, but it does not support the cross-account key policy conditions required for cross-account copy, and some Audit Manager controls treat only customer-managed key vaults as fully compliant.
import * as backup from 'aws-cdk-lib/aws-backup';
import * as kms from 'aws-cdk-lib/aws-kms';
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
const backupKey = new kms.Key(this, 'BackupKey', {
enableKeyRotation: true,
description: 'AliveMCP production backup vault key',
});
const prodVault = new backup.BackupVault(this, 'ProdVault', {
backupVaultName: 'alivemcp-prod',
encryptionKey: backupKey,
lockConfiguration: {
// Start in governance mode — removable by admin during setup validation
minRetentionDays: cdk.Duration.days(7),
// Transition to compliance mode after 3 days (changeable-for-days=3)
// After that transition: permanent, cannot be removed by anyone
},
notificationTopic: backupAlertTopic,
});
A backup plan schedules when to run backups and how long to retain them. Two rules cover most MCP server use cases: a daily rule for granular recent recovery, and a weekly rule for long-term retention. The daily rule should move recovery points to cold storage after 7 days (significant cost reduction for DynamoDB and EFS snapshots) and delete them after 35 days. The weekly rule should move to cold at 30 days and delete at 365 days. Set a generous completion window — 4 hours for daily, 8 hours for weekly — because large EFS file systems can take hours to back up incrementally, and AWS Backup marks a job as expired if it exceeds the completion window, leaving an incomplete recovery point.
const plan = new backup.BackupPlan(this, 'McpBackupPlan', {
backupPlanName: 'alivemcp-prod-plan',
backupVault: prodVault,
backupPlanRules: [
new backup.BackupPlanRule({
ruleName: 'daily-35d',
scheduleExpression: events.Schedule.cron({ hour: '03', minute: '00' }),
startWindow: cdk.Duration.hours(1),
completionWindow: cdk.Duration.hours(4),
deleteAfter: cdk.Duration.days(35),
moveToColdStorageAfter: cdk.Duration.days(7),
}),
new backup.BackupPlanRule({
ruleName: 'weekly-365d',
scheduleExpression: events.Schedule.cron({ weekDay: 'SUN', hour: '04', minute: '00' }),
startWindow: cdk.Duration.hours(2),
completionWindow: cdk.Duration.hours(8),
deleteAfter: cdk.Duration.days(365),
moveToColdStorageAfter: cdk.Duration.days(30),
}),
],
});
Assign resources by tag, not by explicit ARN. Tag-based assignment re-evaluates the selection at every backup job run — the first backup window after a new DynamoDB table is created with the Backup=true tag will include that table automatically, without any IaC update. Use a stack-level tag to apply Backup=true to every resource in the production stack simultaneously, and add Backup=false as an override tag on scratch tables or test infrastructure within the same account that you intentionally want to exclude.
plan.addSelection('TaggedResources', {
backupSelectionName: 'backup-true-tag',
role: iam.Role.fromRoleArn(this, 'BackupRole',
`arn:aws:iam::${this.account}:role/AWSBackupDefaultServiceRole`
),
resources: [
backup.BackupResource.fromTag('Backup', 'true'),
],
});
// Apply to all resources in the production stack
cdk.Tags.of(this).add('Backup', 'true');
DynamoDB PITR and scheduled snapshots are complementary
AWS Backup's scheduled backup of a DynamoDB table creates an on-demand snapshot — a point-in-time image of the table at that moment. PITR (point-in-time recovery) is a separate DynamoDB feature that maintains a continuous change log for the last 35 days, enabling restoration to any second within that window. These two features serve different recovery scenarios: PITR for surgical recovery ("a bug at 14:23:17 wrote corrupted data into 500 sessions — restore to 14:23:00"), scheduled snapshots for long-term retention ("we need a copy of the production table from 6 months ago for audit purposes") and cross-region/cross-account DR. Enable both independently.
new dynamodb.Table(this, 'SessionTable', {
tableName: 'alivemcp-sessions',
partitionKey: { name: 'sessionId', type: dynamodb.AttributeType.STRING },
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
pointInTimeRecovery: true, // PITR: second-granularity for 35 days
encryption: dynamodb.TableEncryption.CUSTOMER_MANAGED,
encryptionKey: backupKey,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
To restore from PITR via CLI — always restores to a new table, never overwrites the source:
aws dynamodb restore-table-to-point-in-time \
--source-table-name alivemcp-sessions \
--target-table-name alivemcp-sessions-restored-2026-11-08 \
--restore-date-time 2026-11-08T02:00:00Z \
--billing-mode-override PAY_PER_REQUEST
Monitoring backup jobs with EventBridge
AWS Backup emits job state change events to EventBridge from the aws.backup source. Subscribe to failure states to alert before an audit cycle reveals silent backup failures. Use a dedicated SNS topic for backup alerts — separate from your PagerDuty oncall topic — so backup job failures get reviewed but do not wake someone up at 3am.
new events.Rule(this, 'BackupJobFailureRule', {
ruleName: 'alivemcp-backup-job-failure',
eventPattern: {
source: ['aws.backup'],
detailType: ['Backup Job State Change'],
detail: {
state: ['FAILED', 'PARTIAL', 'EXPIRED', 'ABORTED'],
},
},
targets: [
new targets.SnsTopic(backupAlertTopic, {
message: events.RuleTargetInput.fromObject({
message: 'AWS Backup job failed',
jobId: events.EventField.fromPath('$.detail.backupJobId'),
resourceArn: events.EventField.fromPath('$.detail.resourceArn'),
state: events.EventField.fromPath('$.detail.state'),
statusMessage: events.EventField.fromPath('$.detail.statusMessage'),
}),
}),
],
});
AWS Backup also publishes to the AWS/Backup CloudWatch namespace. Create a CloudWatch alarm on NumberOfBackupJobsFailed dimensioned by vault name as a belt-and-suspenders alert alongside EventBridge. See the AWS Backup vaults and plans reference for the complete resource type support matrix, cold storage eligibility by resource type, and vault lock governance-to-compliance transition details.
Part 2: Cross-account copy and Organizations disaster recovery
The threat model for backup is not just "what happens if the data is accidentally deleted?" It also includes: what happens if the production AWS account credentials are compromised and an attacker calls backup:DeleteRecoveryPoint on every recovery point in the vault? What happens if AWS has a prolonged regional outage and you need to run your MCP server from a different account while your production account is unavailable? What happens if a team member with broad IAM permissions runs aws backup delete-recovery-point in what they thought was dev but was actually prod?
A backup vault in the same account as your production data cannot protect against any of these scenarios. Cross-account copy solves all three: recovery points are replicated to a separate DR account that has no running production workloads, whose IAM policies do not grant delete access to any production account credential, and that sits in a different AWS account boundary where account-level compromise, misconfigured broad IAM roles, and regional outage impacts are isolated.
Three-account architecture
The recommended setup uses three accounts within an AWS Organization: a production account where all MCP server resources run, a DR account that receives cross-account copies and has no running workloads, and an optional management account that enforces backup policies via Organizations SCPs. The DR account's vault policy explicitly denies delete operations from production account credentials, so even a fully-compromised production account cannot remove DR recovery points.
One-time setup: trusted access and cross-account backup
Three commands from the management account, run once:
# Enable trusted access for AWS Backup with Organizations
aws organizations enable-aws-service-access \
--service-principal backup.amazonaws.com
# Enable cross-account backup in AWS Backup (required in BOTH accounts)
# Run this in the management account AND in each member account
aws backup update-global-settings \
--global-settings '{"isCrossAccountBackupEnabled": "true"}'
# Register the DR account as backup delegated admin
aws organizations register-delegated-administrator \
--account-id DR_ACCOUNT_ID \
--service-principal backup.amazonaws.com
The isCrossAccountBackupEnabled global setting is the most common misconfiguration. It must be true in both the source account and the destination account. Many operators enable it only in the production account, then hit CrossAccountBackupNotEnabled errors when the first copy action runs.
DR vault with cross-account KMS and access policy
Cross-account copies are re-encrypted with the destination vault's KMS key. The DR vault's key must explicitly trust the AWS Backup service principal from the production account to perform the encryption operations:
// CDK in the DR account stack
const drKey = new kms.Key(this, 'DrBackupKey', {
enableKeyRotation: true,
description: 'AliveMCP DR backup vault key',
policy: new iam.PolicyDocument({
statements: [
// Standard admin grant — required so the DR account root can manage the key
new iam.PolicyStatement({
principals: [new iam.AccountRootPrincipal()],
actions: ['kms:*'],
resources: ['*'],
}),
// Cross-account backup service grant — scoped to the production account
new iam.PolicyStatement({
principals: [new iam.ServicePrincipal('backup.amazonaws.com')],
actions: ['kms:Decrypt', 'kms:GenerateDataKey', 'kms:DescribeKey'],
resources: ['*'],
conditions: {
StringEquals: { 'aws:SourceAccount': PROD_ACCOUNT_ID },
},
}),
],
}),
});
const drVault = new backup.BackupVault(this, 'DrVault', {
backupVaultName: 'alivemcp-dr',
encryptionKey: drKey,
accessPolicy: new iam.PolicyDocument({
statements: [
new iam.PolicyStatement({
principals: [new iam.ArnPrincipal(`arn:aws:iam::${PROD_ACCOUNT_ID}:root`)],
actions: ['backup:CopyIntoBackupVault'],
resources: ['*'],
}),
],
}),
});
Adding the copy action to the backup plan
The CDK L2 BackupPlanRule does not expose a copyActions property. Use the CloudFormation escape hatch to add the CopyActions array directly to the CloudFormation rule definition:
// Production account: add cross-account copy to the daily rule
const cfnPlan = plan.node.defaultChild as backup.CfnBackupPlan;
cfnPlan.backupPlan = {
backupPlanName: 'alivemcp-prod-plan',
backupPlanRule: [
{
ruleName: 'daily-with-cross-account-copy',
targetBackupVault: prodVault.backupVaultName,
scheduleExpression: 'cron(0 3 * * ? *)',
startWindowMinutes: 60,
completionWindowMinutes: 240,
lifecycle: {
deleteAfterDays: 35,
moveToColdStorageAfterDays: 7,
},
copyActions: [
{
destinationBackupVaultArn:
`arn:aws:backup:us-west-2:${DR_ACCOUNT_ID}:backup-vault:alivemcp-dr`,
lifecycle: {
deleteAfterDays: 90, // DR copies kept longer than prod hot copies
},
},
],
},
],
};
The copy is asynchronous. The original recovery point reaches COMPLETED status before the copy starts. Monitor copy job status separately — subscribe to Copy Job State Change EventBridge events from aws.backup and alert on FAILED status. A copy failure does not fail the original backup job; both statuses are independent.
Organizations backup policies: enforce coverage across all accounts
Organizations backup policies let the management account attach a backup plan to every member account in an OU without modifying each account's IaC stack. The policy runs alongside each member account's existing plans — it does not replace them. This is how you enforce a minimum backup frequency across a multi-team, multi-account organization: the platform team attaches an org-level policy, and teams cannot reduce coverage below that minimum even if they forget to configure backup in their account-level IaC.
# Organizations backup policy (attached to the production OU)
{
"plans": {
"OrgDailyBackupPlan": {
"regions": { "@@assign": ["us-east-1", "us-west-2"] },
"rules": {
"OrgDailyRule": {
"schedule_expression": { "@@assign": "cron(0 5 * * ? *)" },
"target_backup_vault_name": { "@@assign": "alivemcp-prod" },
"lifecycle": {
"delete_after_days": { "@@assign": 35 },
"move_to_cold_storage_after_days": { "@@assign": 7 }
},
"copy_actions": {
"arn:aws:backup:us-west-2:DR_ACCOUNT_ID:backup-vault:alivemcp-dr": {
"lifecycle": { "delete_after_days": { "@@assign": 90 } }
}
}
}
},
"selections": {
"tags": {
"OrgBackupSelection": {
"iam_role_arn": {
"@@assign": "arn:aws:iam::$account:role/AWSBackupDefaultServiceRole"
},
"tag_key": { "@@assign": "Backup" },
"tag_value": { "@@assign": ["true"] }
}
}
}
}
}
}
The $account macro in the IAM role ARN substitutes each member account's account ID when the policy is applied — each account uses its own AWSBackupDefaultServiceRole, not a cross-account role. See the cross-account backup reference for the complete 3-account architecture, the KMS key policy template, and the DR restore procedure for recovering to a third account after account loss.
Part 3: Automated restore testing
The gap between "we have backups" and "we can recover from backups" is wider than most teams realize. A DynamoDB recovery point can exist in a vault, can show COMPLETED status, and still fail to restore into a usable table — because the IAM role used for restore lacks the right permissions, because the target table name conflicts with an existing table, because the restored table's schema differs from the application's current expectations, or because the recovery point is consistent at the storage level but the data inside is logically corrupted by an application bug that wrote bad records before the backup ran. The only way to know you can recover is to actually do it — preferably on a schedule, automatically, in a staging account, before the incident that forces you to do it under pressure.
AWS Backup Restore Testing automates the full cycle: select a recovery point, run a restore, invoke a validation Lambda, report pass/fail, delete the temporary resource. No manual intervention after initial setup.
Creating the restore testing plan
AWS Backup Restore Testing uses L1 CDK constructs as of late 2026 — the L2 BackupRestoreTestingPlan construct is not yet available. Use CfnRestoreTestingPlan and CfnRestoreTestingSelection:
import { CfnRestoreTestingPlan, CfnRestoreTestingSelection } from 'aws-cdk-lib/aws-backup';
const restoreTestingPlan = new CfnRestoreTestingPlan(this, 'McpRestoreTestingPlan', {
restoreTestingPlanName: 'alivemcp-weekly-restore-test',
scheduleExpression: 'cron(0 6 ? * SUN *)', // weekly Sunday 06:00 UTC
scheduleExpressionTimezone: 'UTC',
startWindowHours: 1,
recoveryPointSelection: {
algorithm: 'LATEST_WITHIN_WINDOW',
includeVaults: [
`arn:aws:backup:us-east-1:${this.account}:backup-vault:alivemcp-prod`,
],
recoveryPointTypes: ['SNAPSHOT'],
selectionWindowDays: 1, // only test recovery points from the last 24 hours
},
});
Two recovery point selection algorithms are available. LATEST_WITHIN_WINDOW always tests your most recent backup — the right choice for a standard weekly confidence check. RANDOM_WITHIN_WINDOW selects a random recovery point within the window — useful for a monthly deep-audit run that validates older recovery points are still restorable, not just the most recent one.
Wire the selection to specific resources and restore parameters:
const restoreTestingSelection = new CfnRestoreTestingSelection(this, 'DynamoRestoreSelection', {
restoreTestingPlanName: restoreTestingPlan.restoreTestingPlanName,
restoreTestingSelectionName: 'dynamodb-alivemcp-sessions',
iamRoleArn: `arn:aws:iam::${this.account}:role/AWSBackupDefaultServiceRole`,
protectedResourceType: 'DynamoDB',
protectedResourceConditions: {
stringEquals: [
{ key: 'aws:ResourceTag/RestoreTesting', value: 'true' },
],
},
restoreMetadataOverrides: {
targetTableName: 'alivemcp-sessions-restore-test',
},
validationWindowHours: 4,
});
The validation Lambda
AWS Backup emits a Restore Testing Job State Change EventBridge event when the restore job reaches COMPLETED status. The validation Lambda subscribes to this event, runs integrity checks against the restored resource, and calls backup:PutRestoreValidationResult within the validation window. If the Lambda does not call PutRestoreValidationResult before the validation window expires, AWS Backup marks the test TIMED_OUT — not FAILED. TIMED_OUT means the system did not get a definitive answer; treat it as a failure requiring investigation.
// validation-lambda/index.ts
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
import { BackupClient, PutRestoreValidationResultCommand } from '@aws-sdk/client-backup';
import { SSMClient, GetParameterCommand } from '@aws-sdk/client-ssm';
const dynamo = new DynamoDBClient({});
const backupClient = new BackupClient({});
const ssm = new SSMClient({});
export const handler = async (event: AWSLambda.EventBridgeEvent<string, any>) => {
const { restoreTestingJobId, createdResourceArn } = event.detail;
const tableName = createdResourceArn.split('/').pop()!;
let validationStatus: 'SUCCESSFUL' | 'FAILED' = 'FAILED';
let validationStatusMessage = '';
try {
// Check 1: table is in ACTIVE state
const describeResult = await dynamo.send(new DescribeTableCommand({ TableName: tableName }));
const tableStatus = describeResult.Table?.TableStatus;
if (tableStatus !== 'ACTIVE') {
throw new Error(`Table status is ${tableStatus}, expected ACTIVE`);
}
// Check 2: row count within 10% of expected production count
const { Parameter } = await ssm.send(new GetParameterCommand({
Name: '/alivemcp/sessions-table-row-count',
}));
const expectedCount = parseInt(Parameter?.Value ?? '0', 10);
const actualCount = describeResult.Table?.ItemCount ?? 0;
const delta = Math.abs(actualCount - expectedCount) / (expectedCount || 1);
if (delta > 0.1) {
throw new Error(
`Row count mismatch: expected ~${expectedCount}, got ${actualCount} (${(delta * 100).toFixed(1)}% delta)`
);
}
// Check 3: test write-and-delete to verify table is writable
const testKey = { sessionId: { S: `restore-test-${Date.now()}` } };
const ddbFull = await import('@aws-sdk/client-dynamodb');
await dynamo.send(new ddbFull.PutItemCommand({
TableName: tableName,
Item: { ...testKey, createdAt: { S: new Date().toISOString() }, testWrite: { BOOL: true } },
}));
await dynamo.send(new ddbFull.DeleteItemCommand({ TableName: tableName, Key: testKey }));
validationStatus = 'SUCCESSFUL';
validationStatusMessage = `Table ${tableName} ACTIVE, ~${actualCount} items, write test passed`;
} catch (err) {
validationStatusMessage = (err as Error).message;
}
await backupClient.send(new PutRestoreValidationResultCommand({
RestoreJobId: restoreTestingJobId,
ValidationStatus: validationStatus,
ValidationStatusMessage: validationStatusMessage,
}));
};
Wire the Lambda to EventBridge and grant required IAM permissions:
new events.Rule(this, 'RestoreTestingCompletedRule', {
eventPattern: {
source: ['aws.backup'],
detailType: ['Restore Testing Job State Change'],
detail: {
status: ['COMPLETED'],
restoreTestingPlanName: ['alivemcp-weekly-restore-test'],
},
},
targets: [new targets.LambdaFunction(validationLambda)],
});
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['backup:PutRestoreValidationResult'],
resources: ['*'],
}));
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['ssm:GetParameter'],
resources: [`arn:aws:ssm:${this.region}:${this.account}:parameter/alivemcp/*`],
}));
validationLambda.addToRolePolicy(new iam.PolicyStatement({
actions: ['dynamodb:DescribeTable', 'dynamodb:PutItem', 'dynamodb:DeleteItem'],
// Wildcard suffix — the restored table name includes a timestamp suffix
resources: [`arn:aws:dynamodb:${this.region}:${this.account}:table/alivemcp-sessions-restore-test*`],
}));
Keeping the expected row count current
The validation Lambda compares actual row count against an expected value in SSM. DynamoDB's ItemCount from DescribeTable is approximate — updated every 6 hours — so a 10% tolerance window is appropriate. Update the SSM parameter nightly from a lightweight scheduled Lambda:
// nightly-row-count-updater/index.ts
import { DynamoDBClient, DescribeTableCommand } from '@aws-sdk/client-dynamodb';
import { SSMClient, PutParameterCommand } from '@aws-sdk/client-ssm';
export const handler = async () => {
const dynamo = new DynamoDBClient({});
const ssm = new SSMClient({});
const result = await dynamo.send(new DescribeTableCommand({ TableName: 'alivemcp-sessions' }));
const count = result.Table?.ItemCount ?? 0;
await ssm.send(new PutParameterCommand({
Name: '/alivemcp/sessions-table-row-count',
Value: String(count),
Type: 'String',
Overwrite: true,
}));
};
Pair restore testing with AWS FIS chaos experiments for a complete resilience validation loop — FIS chaos engineering validates how your MCP server behaves under failure, while restore testing validates that your data layer can be recovered after failure. See the restore testing reference for the complete lifecycle diagram, all five common failure modes and fixes, and how to interpret TIMED_OUT vs FAILED results from the restore testing job API.
Part 4: Audit Manager compliance reporting
When an enterprise customer asks during a security questionnaire "can you show evidence that every production database is backed up daily and that backups are encrypted?" you need more than a screenshot from the AWS console taken this morning. You need a machine-readable compliance report generated automatically for every day of the last 12 months, covering every resource in scope, showing whether each backup control was satisfied and — if not — which specific resource was non-compliant and why.
AWS Backup Audit Manager generates exactly this evidence. It continuously evaluates every protected resource in your account against named controls, records the per-resource compliance status, and exports daily reports to S3. Auditors can download the reports for the entire audit period without you having to do anything after initial setup.
Five critical controls for MCP servers
AWS Backup Audit Manager provides 14 built-in controls. For MCP servers, these five are the most critical:
| Control | What it checks | Key parameter |
|---|---|---|
BACKUP_PLAN_MIN_FREQUENCY_AND_MIN_RETENTION_CHECK | Every backup plan rule runs at least every N days and retains recovery points at least M days | requiredFrequencyValue=1 (daily), requiredRetentionDays=35 |
BACKUP_RECOVERY_POINT_ENCRYPTED | All recovery points in the specified resource types are encrypted (checks vault KMS encryption) | Scope: DynamoDB, EFS, RDS, EBS |
BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN | Every resource of specified types is assigned to at least one backup plan | resourceType=DynamoDB,EFS,RDS |
BACKUP_RECOVERY_POINT_MINIMUM_RETENTION_CHECK | All recovery points have retention of at least N days | requiredRetentionDays=35 |
BACKUP_LAST_RECOVERY_POINT_CREATED | At least one recovery point was created within the last N hours for each protected resource | recoveryPointAgeValue=26 (not 24 — 2-hour buffer for backup jobs that complete at hour 25) |
Creating the framework via CLI
Use CLI for framework creation — it is repeatable and version-controllable, unlike console setup:
aws backup create-framework \
--framework-name "alivemcp-backup-compliance" \
--framework-controls '[
{
"ControlName": "BACKUP_PLAN_MIN_FREQUENCY_AND_MIN_RETENTION_CHECK",
"ControlInputParameters": [
{"ParameterName": "requiredFrequencyUnit", "ParameterValue": "days"},
{"ParameterName": "requiredFrequencyValue", "ParameterValue": "1"},
{"ParameterName": "requiredRetentionDays", "ParameterValue": "35"}
],
"ControlScope": { "Tags": {"Backup": "true"} }
},
{
"ControlName": "BACKUP_RECOVERY_POINT_ENCRYPTED",
"ControlScope": {
"ComplianceResourceTypes": ["DynamoDB", "EFS", "RDS", "EBS"]
}
},
{
"ControlName": "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN",
"ControlInputParameters": [
{"ParameterName": "resourceType", "ParameterValue": "DynamoDB,EFS,RDS"}
],
"ControlScope": { "Tags": {"Backup": "true"} }
},
{
"ControlName": "BACKUP_RECOVERY_POINT_MINIMUM_RETENTION_CHECK",
"ControlInputParameters": [
{"ParameterName": "requiredRetentionDays", "ParameterValue": "35"}
]
},
{
"ControlName": "BACKUP_LAST_RECOVERY_POINT_CREATED",
"ControlInputParameters": [
{"ParameterName": "recoveryPointAgeUnit", "ParameterValue": "hours"},
{"ParameterName": "recoveryPointAgeValue", "ParameterValue": "26"}
],
"ControlScope": { "Tags": {"Backup": "true"} }
}
]'
Scope controls to Backup=true tagged resources consistently with your backup plan's resource assignment. This ensures you don't get false compliance failures for scratch tables that are intentionally unprotected — only the resources you've opted into the backup program are evaluated by the framework.
Daily compliance reports to S3
Create a report plan to export daily compliance evidence:
aws backup create-report-plan \
--report-plan-name "alivemcp-daily-compliance-report" \
--report-delivery-channel '{
"S3BucketName": "alivemcp-compliance-reports",
"S3KeyPrefix": "backup/",
"Formats": ["CSV", "JSON"]
}' \
--report-setting '{
"ReportTemplate": "RESOURCE_COMPLIANCE_REPORT",
"FrameworkArns": [
"arn:aws:backup:us-east-1:ACCOUNT_ID:framework/alivemcp-backup-compliance"
]
}'
AWS Backup generates reports once per day at approximately 05:00 UTC covering the previous 24 hours. For SOC 2 Type II, request both RESOURCE_COMPLIANCE_REPORT (per-DynamoDB-table compliance status for each control) and BACKUP_JOB_REPORT (all backup jobs run during the audit period with success/failure status) for every month of the audit window. The S3 bucket needs a policy granting s3:PutObject to the reports.backup.amazonaws.com service principal — without this, reports silently fail to generate.
Mapping controls to audit frameworks
| Control | SOC 2 (CC9.1) | HIPAA §164.308(a)(7) | ISO 27001 A.12.3.1 |
|---|---|---|---|
| BACKUP_PLAN_MIN_FREQUENCY | Recovery time + point objectives | Contingency plan — data backup | Information backup |
| BACKUP_RECOVERY_POINT_ENCRYPTED | Encryption at rest | Encryption of PHI at rest | Cryptographic controls |
| BACKUP_RESOURCES_PROTECTED | System availability controls | Data backup completeness | Information backup scope |
| BACKUP_MIN_RETENTION_CHECK | Retention period controls | 7-year PHI retention requirement | Backup retention schedule |
| BACKUP_LAST_RECOVERY_POINT | Continuous availability evidence | Contingency plan testing | Backup timeliness verification |
Querying compliance status in real time
For security questionnaire responses, query compliance status directly instead of waiting for the next daily report:
# List all controls with current compliance status
aws backup list-framework-controls \
--framework-name alivemcp-backup-compliance \
--query 'FrameworkControls[*].{Control:ControlName,Status:ControlStatus}'
# List non-compliant resources for immediate investigation
aws backup list-compliance-results \
--framework-name alivemcp-backup-compliance \
--query 'Results[?ComplianceType==`NON_COMPLIANT`].{Resource:ResourceArn,Control:ControlName}'
Automatic remediation for non-compliant resources
Each Audit Manager control maps to a managed AWS Config rule under the hood. Attach an SSM Automation remediation action to the Config rule for BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN to automatically apply Backup=true to any untagged resource:
aws configservice put-remediation-configurations --cli-input-json '{
"RemediationConfigurations": [{
"ConfigRuleName": "backup-resources-protected-by-backup-plan",
"TargetType": "SSM_DOCUMENT",
"TargetId": "AWS-AddTagsToResource",
"Parameters": {
"ResourceArn": { "ResourceValue": { "Value": "RESOURCE_ID" } },
"Tags": { "StaticValue": { "Values": ["{\"Backup\":\"true\"}"] } }
},
"Automatic": true,
"MaximumAutomaticAttempts": 3,
"RetryAttemptSeconds": 60
}]
}'
Automatic remediation is an aggressive stance — it tags every unprotected resource without review. For early-stage MCP server teams where tagging conventions are still evolving, manual remediation (weekly review of the Config compliance report, manual tag additions) is safer. Enable automatic remediation once your tagging strategy is stable and you are confident that any resource that should be excluded from backup is correctly tagged Backup=false. See the Audit Manager reference for the complete control parameter table, CDK L1 framework definition, all five report templates, and the two most common compliance report delivery failures.
Putting the full strategy together
AliveMCP's monitoring data shows a consistent pattern in MCP server reliability: operators who have invested in a layered data protection strategy — backup vaults with vault lock, cross-account copies, automated restore testing, and compliance reporting — experience fewer extended outages when incidents do occur. Not because those incidents happen less often, but because when they do happen, the operator has already practiced the recovery path. They know the restore takes 15 minutes for the sessions table. They know the validation Lambda reports SUCCESSFUL. They know the DR vault in the separate account has 90 days of recovery points. They have the CLI commands ready. The incident becomes a known procedure, not an improvised emergency.
The four parts of this guide form a natural progression: start with vaults and plans (you need recovery points before anything else), add cross-account copy (remove the same-account threat model), add restore testing (validate that the recovery points are usable), and add Audit Manager (generate compliance evidence continuously so it is ready when you need it). Each layer is independently valuable — a team that only implements the first two layers is dramatically better protected than a team with no backup strategy. But the full stack is the target, and AWS Backup's unified control plane makes all four layers manageable within a single CDK stack and a few hundred lines of CloudFormation.
Keep your MCP server's backup health visible alongside its availability health. AliveMCP monitors endpoint reachability — if you are also monitoring backup job failure via the EventBridge rule shown in Part 1 and surfacing Audit Manager non-compliance via the CLI queries in Part 4, you have a complete operational picture: "the MCP server is alive, its backups are running, and those backups have been tested and are compliant." That is a materially different level of confidence than "the MCP server is alive and we think we have backups."