diff --git a/dynamodb-global-tables-multi-account-cdk/.gitignore b/dynamodb-global-tables-multi-account-cdk/.gitignore new file mode 100644 index 000000000..745e8db83 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/.gitignore @@ -0,0 +1,6 @@ +*.js +*.d.ts +node_modules +cdk.out +cdk.context.json +package-lock.json diff --git a/dynamodb-global-tables-multi-account-cdk/README.md b/dynamodb-global-tables-multi-account-cdk/README.md new file mode 100644 index 000000000..0fd4594c3 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/README.md @@ -0,0 +1,111 @@ +# Amazon DynamoDB Global Tables — True Multi-Account Replication + +This pattern deploys a DynamoDB Global Table that replicates data across two separate AWS accounts using `TableV2MultiAccountReplica`. Writes in Account A are automatically replicated to Account B with sub-second latency. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/dynamodb-global-tables-multi-account-cdk + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [AWS CDK](https://docs.aws.amazon.com/cdk/latest/guide/cli.html) installed (v2.252.0+) +* [Node.js](https://nodejs.org/en/download/) 20.x or later +* Two AWS accounts with credentials configured +* CDK bootstrapped in both accounts/regions + +## Architecture + +``` +┌─────────────────────────────────┐ ┌─────────────────────────────────┐ +│ Account A (742460038667) │ │ Account B (843577947854) │ +│ us-east-1 │ │ us-west-2 │ +│ │ │ │ +│ ┌───────────────────────────┐ │ auto │ ┌───────────────────────────┐ │ +│ │ DynamoDB GlobalTable │──┼────────▶│ │ DynamoDB Replica (CFN) │ │ +│ │ MultiAccountGlobalTable │ │ replicate │ MultiAccountGlobalTable │ │ +│ └───────────────────────────┘ │ │ └───────────────────────────┘ │ +│ │ │ │ +│ Source Stack │ │ Replica Stack │ +└─────────────────────────────────┘ └─────────────────────────────────┘ +``` + +## How it works + +1. **Source Stack** (Account A): Creates a DynamoDB Global Table with on-demand billing and point-in-time recovery +2. **Replica Stack** (Account B): Uses `TableV2MultiAccountReplica` to join the Global Table as a replica in a different account and region +3. DynamoDB automatically handles replication — writes in either account propagate to the other with sub-second latency +4. CDK automatically configures the resource policies needed for cross-account replication + +**Note:** The replica must be in a different region from the source. This is a DynamoDB Global Tables requirement. + +## Deployment Instructions + +1. Clone and install: + ```bash + cd serverless-patterns/dynamodb-global-tables-multi-account-cdk + npm install + ``` + +2. Bootstrap CDK in both accounts (if not already done): + ```bash + npx cdk bootstrap aws://ACCOUNT_A/us-east-1 --profile source-profile + npx cdk bootstrap aws://ACCOUNT_B/us-west-2 --profile replica-profile + ``` + +3. Deploy the source stack first: + ```bash + npx cdk deploy DynamoDbMultiAccountSourceStack \ + -c sourceAccount=ACCOUNT_A \ + -c sourceRegion=us-east-1 \ + -c replicaAccount=ACCOUNT_B \ + -c replicaRegion=us-west-2 \ + --profile source-profile + ``` + +4. Deploy the replica stack: + ```bash + npx cdk deploy DynamoDbMultiAccountReplicaStack \ + -c sourceAccount=ACCOUNT_A \ + -c sourceRegion=us-east-1 \ + -c replicaAccount=ACCOUNT_B \ + -c replicaRegion=us-west-2 \ + --profile replica-profile + ``` + +## Testing + +```bash +# Write an item in Account A (source) +aws dynamodb put-item \ + --table-name MultiAccountGlobalTable \ + --item '{"PK":{"S":"user#123"},"SK":{"S":"profile"},"name":{"S":"test"}}' \ + --region us-east-1 --profile source-profile + +# Read the item from Account B (replica) — should appear within ~1 second +aws dynamodb get-item \ + --table-name MultiAccountGlobalTable \ + --key '{"PK":{"S":"user#123"},"SK":{"S":"profile"}}' \ + --region us-west-2 --profile replica-profile +``` + +## Cleanup + +Destroy in reverse order: +```bash +npx cdk destroy DynamoDbMultiAccountReplicaStack \ + -c sourceAccount=ACCOUNT_A -c sourceRegion=us-east-1 \ + -c replicaAccount=ACCOUNT_B -c replicaRegion=us-west-2 \ + --profile replica-profile + +npx cdk destroy DynamoDbMultiAccountSourceStack \ + -c sourceAccount=ACCOUNT_A -c sourceRegion=us-east-1 \ + -c replicaAccount=ACCOUNT_B -c replicaRegion=us-west-2 \ + --profile source-profile +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/dynamodb-global-tables-multi-account-cdk/bin/app.ts b/dynamodb-global-tables-multi-account-cdk/bin/app.ts new file mode 100644 index 000000000..5c1145b51 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/bin/app.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { SourceTableStack } from '../lib/source-table-stack'; +import { ReplicaTableStack } from '../lib/replica-table-stack'; + +const app = new cdk.App(); + +const sourceAccount = app.node.tryGetContext('sourceAccount'); +const sourceRegion = app.node.tryGetContext('sourceRegion') || 'us-east-1'; +const replicaAccount = app.node.tryGetContext('replicaAccount'); +const replicaRegion = app.node.tryGetContext('replicaRegion') || 'us-east-1'; + +const sourceStack = new SourceTableStack(app, 'DynamoDbMultiAccountSourceStack', { + env: { account: sourceAccount, region: sourceRegion }, +}); + +new ReplicaTableStack(app, 'DynamoDbMultiAccountReplicaStack', { + env: { account: replicaAccount, region: replicaRegion }, + sourceTable: sourceStack.table, +}); diff --git a/dynamodb-global-tables-multi-account-cdk/cdk.json b/dynamodb-global-tables-multi-account-cdk/cdk.json new file mode 100644 index 000000000..a6700a2ff --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/dynamodb-global-tables-multi-account-cdk/example-pattern.json b/dynamodb-global-tables-multi-account-cdk/example-pattern.json new file mode 100644 index 000000000..973909db3 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/example-pattern.json @@ -0,0 +1,41 @@ +{ + "title": "Amazon DynamoDB Global Tables with True Multi-Account Replication", + "description": "Deploy a DynamoDB Global Table that replicates across two separate AWS accounts using TableV2MultiAccountReplica. Deployed with AWS CDK.", + "language": "TypeScript", + "level": "300", + "framework": "AWS CDK", + "introBox": { + "headline": "How it works", + "text": [ + "This pattern creates a DynamoDB Global Table in Account A and a multi-account replica in Account B using TableV2MultiAccountReplica.", + "Writes in either account are automatically replicated to the other with sub-second latency.", + "CDK handles the cross-account resource policies needed for replication.", + "The replica must be in a different region from the source (Global Tables requirement)." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/dynamodb-global-tables-multi-account-cdk", + "templateURL": "serverless-patterns/dynamodb-global-tables-multi-account-cdk", + "projectFolder": "dynamodb-global-tables-multi-account-cdk", + "templateFile": "lib/source-table-stack.ts" + } + }, + "resources": { + "bullets": [ + { "text": "DynamoDB Global Tables cross-account replication", "link": "https://aws.amazon.com/blogs/database/amazon-dynamodb-global-tables-now-support-replication-across-aws-accounts/" }, + { "text": "CDK TableV2MultiAccountReplica", "link": "https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_dynamodb.TableV2MultiAccountReplica.html" }, + { "text": "DynamoDB Global Tables documentation", "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html" } + ] + }, + "deploy": { "text": ["See README for multi-account deployment steps"] }, + "testing": { "text": ["See the README for testing instructions."] }, + "cleanup": { "text": ["cdk destroy in reverse order (replica first, then source)"] }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ] +} diff --git a/dynamodb-global-tables-multi-account-cdk/lib/replica-table-stack.ts b/dynamodb-global-tables-multi-account-cdk/lib/replica-table-stack.ts new file mode 100644 index 000000000..9938e5620 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/lib/replica-table-stack.ts @@ -0,0 +1,26 @@ +import * as cdk from 'aws-cdk-lib'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import { Construct } from 'constructs'; + +export interface ReplicaTableStackProps extends cdk.StackProps { + sourceTable: dynamodb.ITableV2; +} + +export class ReplicaTableStack extends cdk.Stack { + public readonly replica: dynamodb.TableV2MultiAccountReplica; + + constructor(scope: Construct, id: string, props: ReplicaTableStackProps) { + super(scope, id, props); + + this.replica = new dynamodb.TableV2MultiAccountReplica(this, 'ReplicaTable', { + tableName: 'MultiAccountGlobalTable', + replicaSourceTable: props.sourceTable, + globalTableSettingsReplicationMode: dynamodb.GlobalTableSettingsReplicationMode.ALL, + removalPolicy: cdk.RemovalPolicy.DESTROY, + pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true }, + }); + + new cdk.CfnOutput(this, 'ReplicaTableName', { value: this.replica.tableName }); + new cdk.CfnOutput(this, 'ReplicaTableArn', { value: this.replica.tableArn }); + } +} diff --git a/dynamodb-global-tables-multi-account-cdk/lib/source-table-stack.ts b/dynamodb-global-tables-multi-account-cdk/lib/source-table-stack.ts new file mode 100644 index 000000000..680a3ed8f --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/lib/source-table-stack.ts @@ -0,0 +1,24 @@ +import * as cdk from 'aws-cdk-lib'; +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; +import { Construct } from 'constructs'; + +export class SourceTableStack extends cdk.Stack { + public readonly table: dynamodb.TableV2; + + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + this.table = new dynamodb.TableV2(this, 'SourceTable', { + tableName: 'MultiAccountGlobalTable', + partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING }, + sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING }, + billing: dynamodb.Billing.onDemand(), + pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true }, + globalTableSettingsReplicationMode: dynamodb.GlobalTableSettingsReplicationMode.ALL, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + new cdk.CfnOutput(this, 'TableName', { value: this.table.tableName }); + new cdk.CfnOutput(this, 'TableArn', { value: this.table.tableArn }); + } +} diff --git a/dynamodb-global-tables-multi-account-cdk/package.json b/dynamodb-global-tables-multi-account-cdk/package.json new file mode 100644 index 000000000..769be3265 --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/package.json @@ -0,0 +1,15 @@ +{ + "name": "dynamodb-global-tables-multi-account-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.ts" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "devDependencies": { + "aws-cdk": "^2.258.0", + "typescript": "~5.7.0", + "ts-node": "^10.9.2" + }, + "dependencies": { + "aws-cdk-lib": "^2.258.0", + "constructs": "^10.0.0" + } +} diff --git a/dynamodb-global-tables-multi-account-cdk/tsconfig.json b/dynamodb-global-tables-multi-account-cdk/tsconfig.json new file mode 100644 index 000000000..8bd74744e --- /dev/null +++ b/dynamodb-global-tables-multi-account-cdk/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "outDir": "build", + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "exclude": ["node_modules", "cdk.out"] +}