-
Notifications
You must be signed in to change notification settings - Fork 1k
New pattern - dynamodb-cross-account-replication-cdk #3099
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
a94c30e
8e72ec4
4f302fd
156f148
830d625
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| # Amazon DynamoDB Global Tables with Cross-Account Read Access | ||
|
|
||
| This pattern deploys an Amazon DynamoDB Global Table (same-account, multi-region) and an IAM role that grants a separate AWS account read-only access to the table in both regions. | ||
|
|
||
| Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/dynamodb-cross-account-replication-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 | ||
| * [Node.js](https://nodejs.org/en/download/) 20.x or later | ||
| * Two AWS accounts (one owns the table, the other reads from it) | ||
| * CDK bootstrapped in the deployment account/region (`cdk bootstrap`) | ||
|
|
||
| ## How it works | ||
|
|
||
| ``` | ||
| ┌─────────────────────────────────────────────────────────────────┐ | ||
| │ Owner Account │ | ||
| │ │ | ||
| │ ┌─────────────────┐ Global Tables ┌─────────────────┐ │ | ||
| │ │ DynamoDB Table │ ─────────────────▶ │ DynamoDB Replica │ │ | ||
| │ │ (us-east-1) │ auto-replicate │ (us-west-2) │ │ | ||
| │ └─────────────────┘ └─────────────────┘ │ | ||
| │ │ │ │ | ||
| │ └───────────── IAM Role ───────────────┘ │ | ||
| │ │ │ | ||
| └──────────────────────────────┼────────────────────────────────────┘ | ||
| │ AssumeRole | ||
| ┌──────────────────────────────┼────────────────────────────────────┐ | ||
| │ Reader Account ▼ │ | ||
| │ GetItem / Query / Scan │ | ||
| └───────────────────────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| - A DynamoDB Global Table is created with a replica in the specified region (same account) | ||
| - An IAM role is created that can be assumed by the reader account | ||
| - The role grants read-only access (`GetItem`, `Query`, `Scan`, `BatchGetItem`) to the table in **both** regions | ||
| - DynamoDB handles replication automatically with sub-second latency | ||
|
|
||
| **Note:** This is same-account multi-region replication (Global Tables) with cross-account read access via IAM. For true cross-account Global Tables replication, use `TableV2MultiAccountReplica` in a separate stack deployed to the replica account. | ||
|
|
||
| ## Deployment Instructions | ||
|
|
||
| 1. Clone and navigate to the pattern: | ||
| ```bash | ||
| cd serverless-patterns/dynamodb-cross-account-replication-cdk | ||
| npm install | ||
| ``` | ||
| 2. Deploy with the reader account ID and replica region: | ||
| ```bash | ||
| cdk deploy --parameters ReplicaAccountId=123456789012 -c replicaRegion=us-west-2 | ||
| ``` | ||
|
|
||
| ## Testing | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No cross-account read test in the Testing section. The current Testing section only shows reads from the same account. The pattern's headline value is cross-account read, so please demonstrate it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair -- added a cross-account read example using sts assume-role in the Testing section. |
||
|
|
||
| ```bash | ||
| # Write an item to the source table | ||
| aws dynamodb put-item --table-name $(aws cloudformation describe-stacks \ | ||
| --stack-name DynamodbCrossAccountReplicationStack \ | ||
| --query 'Stacks[0].Outputs[?OutputKey==`TableName`].OutputValue' --output text) \ | ||
| --item '{"PK":{"S":"user#123"},"SK":{"S":"profile"},"name":{"S":"test"}}' | ||
|
|
||
| # Read from replica region (same account, verifies replication) | ||
| aws dynamodb get-item --table-name <TableName> \ | ||
| --key '{"PK":{"S":"user#123"},"SK":{"S":"profile"}}' \ | ||
| --region us-west-2 | ||
|
|
||
| # Cross-account read (from reader account, assuming the role) | ||
| aws sts assume-role --role-arn <CrossAccountRoleArn> \ | ||
| --role-session-name reader-test | ||
|
|
||
| # Export the temporary credentials | ||
| export AWS_ACCESS_KEY_ID=<AccessKeyId from above> | ||
| export AWS_SECRET_ACCESS_KEY=<SecretAccessKey from above> | ||
| export AWS_SESSION_TOKEN=<SessionToken from above> | ||
|
|
||
| # Read from the replica region using the cross-account role | ||
| aws dynamodb get-item --table-name <TableName> \ | ||
| --key '{"PK":{"S":"user#123"},"SK":{"S":"profile"}}' \ | ||
| --region us-west-2 | ||
| ``` | ||
|
|
||
| ## Cleanup | ||
|
|
||
| > **⚠️ Warning:** `cdk destroy` with `RemovalPolicy.DESTROY` will permanently delete the table and all its data. Back up any important data before destroying. | ||
|
|
||
| ```bash | ||
| cdk destroy | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cleanup must warn that
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call -- added a warning note in the Cleanup section about data deletion with RemovalPolicy.DESTROY. |
||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
|
|
||
| SPDX-License-Identifier: MIT-0 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| #!/usr/bin/env node | ||
| import 'source-map-support/register'; | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import { DynamodbCrossAccountReplicationStack } from '../lib/dynamodb-cross-account-replication-stack'; | ||
|
|
||
| const app = new cdk.App(); | ||
|
|
||
| const account = process.env.CDK_DEFAULT_ACCOUNT; | ||
| const region = process.env.CDK_DEFAULT_REGION || 'us-east-1'; | ||
|
|
||
| if (!account) { | ||
| throw new Error('CDK_DEFAULT_ACCOUNT is required. Run: export CDK_DEFAULT_ACCOUNT=$(aws sts get-caller-identity --query Account --output text)'); | ||
| } | ||
|
|
||
| new DynamodbCrossAccountReplicationStack(app, 'DynamodbCrossAccountReplicationStack', { | ||
| env: { account, region }, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| { | ||
| "app": "npx ts-node --prefer-ts-exts bin/app.ts" | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| { | ||
| "title": "Amazon DynamoDB Global Tables with Cross-Account Read Access", | ||
| "description": "Deploy a DynamoDB Global Table with a cross-account IAM role for secure read-only access from another AWS account.", | ||
| "language": "TypeScript", | ||
| "level": "300", | ||
| "framework": "AWS CDK", | ||
| "introBox": { | ||
| "headline": "How it works", | ||
| "text": [ | ||
| "This pattern creates a DynamoDB Global Table (same-account, multi-region) with an IAM role for cross-account read access.", | ||
| "The reader account assumes the IAM role and can query the table in either region.", | ||
| "DynamoDB handles replication automatically with sub-second latency between regions.", | ||
| "Point-in-time recovery is enabled for data protection." | ||
| ] | ||
| }, | ||
| "gitHub": { | ||
| "template": { | ||
| "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/dynamodb-cross-account-replication-cdk", | ||
| "templateURL": "serverless-patterns/dynamodb-cross-account-replication-cdk", | ||
| "projectFolder": "dynamodb-cross-account-replication-cdk", | ||
| "templateFile": "lib/dynamodb-cross-account-replication-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": "DynamoDB Global Tables Documentation", "link": "https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GlobalTables.html" } | ||
| ] | ||
| }, | ||
| "deploy": { "text": ["cdk deploy --parameters ReplicaAccountId=123456789012"] }, | ||
| "testing": { "text": ["See the README for testing instructions."] }, | ||
| "cleanup": { "text": ["cdk destroy"] }, | ||
| "authors": [ | ||
| { | ||
| "name": "Nithin Chandran R", | ||
| "bio": "Technical Account Manager at AWS, passionate about serverless and AI/ML.", | ||
| "linkedin": "nithin-chandran-r" | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import * as cdk from 'aws-cdk-lib'; | ||
| import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; | ||
| import * as iam from 'aws-cdk-lib/aws-iam'; | ||
| import { Construct } from 'constructs'; | ||
|
|
||
| export class DynamodbCrossAccountReplicationStack extends cdk.Stack { | ||
| constructor(scope: Construct, id: string, props?: cdk.StackProps) { | ||
| super(scope, id, props); | ||
|
|
||
| const replicaAccountId = new cdk.CfnParameter(this, 'ReplicaAccountId', { | ||
| type: 'String', | ||
| description: 'AWS Account ID that will assume the cross-account read role', | ||
| }); | ||
|
|
||
| // Replica region must be a literal (TableV2 does not accept tokens for replica regions) | ||
| const replicaRegion = this.node.tryGetContext('replicaRegion') || 'us-west-2'; | ||
|
|
||
| const table = new dynamodb.TableV2(this, 'SourceTable', { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. KMS expectations for cross-account reads are not addressed.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The table uses the default AWS-owned key (no explicit KMS config), so cross-account reads work without any key policy changes. Added a note in the README that if customers switch to CMK encryption they'll need to grant kms:Decrypt to the cross-account role. |
||
| partitionKey: { name: 'PK', type: dynamodb.AttributeType.STRING }, | ||
| sortKey: { name: 'SK', type: dynamodb.AttributeType.STRING }, | ||
| billing: dynamodb.Billing.onDemand(), | ||
| pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true }, | ||
| // DESTROY for easy cleanup in sample patterns. Use RETAIN in production. | ||
| removalPolicy: cdk.RemovalPolicy.DESTROY, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The pattern frames itself as a cross-account replication solution and turns on PITR, both signal a production lean. Pairing that with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. Added inline comment in code explaining DESTROY is for sample pattern testability. Cleanup section already warns about data deletion. Kept DESTROY since RETAIN would leave orphaned tables for users just trying the pattern. |
||
| replicas: [{ region: replicaRegion }], | ||
| }); | ||
|
Comment on lines
+13
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Title says "cross-account replication" but the implementation is same-account, multi-region. Possible ways to resolve:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed -- reframed the whole pattern. Title is now "DynamoDB Global Tables with Cross-Account Read Access", README explicitly calls out that replication is same-account and the cross-account piece is the IAM role. |
||
|
|
||
| // Cross-account read role — grants access to BOTH source and replica region ARNs | ||
| const crossAccountRole = new iam.Role(this, 'CrossAccountReadRole', { | ||
| assumedBy: new iam.AccountPrincipal(replicaAccountId.valueAsString), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trust policy allows any principal in the replica account; no
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True -- it trusts the entire account. For a sample pattern this keeps it simple. Adding ExternalId or role-name conditions would make it more production-ready but also more complex to demo. Happy to add an ExternalId condition if you feel strongly about it. |
||
| description: 'Allows another account to read from the DynamoDB Global Table in any region', | ||
| }); | ||
|
|
||
| crossAccountRole.addToPolicy(new iam.PolicyStatement({ | ||
| actions: [ | ||
| 'dynamodb:GetItem', | ||
| 'dynamodb:Query', | ||
| 'dynamodb:BatchGetItem', | ||
| ], | ||
|
Comment on lines
+36
to
+39
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point -- dropped Scan. GetItem, Query, and BatchGetItem cover the typical cross-account read use cases. |
||
| resources: [ | ||
| // Source region | ||
| table.tableArn, | ||
| `${table.tableArn}/index/*`, | ||
| // Replica region (table.tableArn is source-region-scoped) | ||
| `arn:aws:dynamodb:${replicaRegion}:${this.account}:table/${table.tableName}`, | ||
| `arn:aws:dynamodb:${replicaRegion}:${this.account}:table/${table.tableName}/index/*`, | ||
| ], | ||
| })); | ||
|
|
||
| new cdk.CfnOutput(this, 'TableName', { value: table.tableName }); | ||
| new cdk.CfnOutput(this, 'TableArn', { value: table.tableArn }); | ||
| new cdk.CfnOutput(this, 'CrossAccountRoleArn', { value: crossAccountRole.roleArn }); | ||
| new cdk.CfnOutput(this, 'ReplicaRegion', { value: replicaRegion }); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "name": "dynamodb-cross-account-replication-cdk", | ||
| "version": "1.0.0", | ||
| "bin": { "app": "bin/app.ts" }, | ||
| "scripts": { "build": "tsc", "cdk": "cdk" }, | ||
| "dependencies": { | ||
| "aws-cdk-lib": "^2.180.0", | ||
| "constructs": "^10.0.0", | ||
| "source-map-support": "^0.5.21" | ||
| }, | ||
| "devDependencies": { | ||
| "typescript": "~5.4.0", | ||
| "ts-node": "^10.9.0", | ||
| "@types/node": "^20.0.0" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "target": "ES2020", "module": "commonjs", "lib": ["es2020"], | ||
| "declaration": true, "strict": true, "outDir": "build", | ||
| "rootDir": ".", "skipLibCheck": true, "forceConsistentCasingInFileNames": true | ||
| }, | ||
| "exclude": ["node_modules", "build"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cdk bootstrapis missing from Requirements. New users may hit AWS CDK bootstrap errors immediately.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added to requirements. Thanks.