Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions dynamodb-cross-account-replication-cdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Amazon DynamoDB Cross-Account Replication with Global Tables

This pattern deploys a DynamoDB Global Table with cross-account replication and an IAM role for secure cross-account read access.

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/) installed
* Two AWS accounts (source and replica)

## Deployment Instructions

1. Clone and navigate to the pattern:
```
cd serverless-patterns/dynamodb-cross-account-replication-cdk
Comment on lines +6 to +49

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cdk bootstrap is missing from Requirements. New users may hit AWS CDK bootstrap errors immediately.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to requirements. Thanks.

npm install
```
2. Deploy with the replica account ID:
```
cdk deploy --parameters ReplicaAccountId=123456789012 --parameters ReplicaRegion=us-west-2
```

@parikhudit parikhudit Jun 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--parameters ReplicaRegion=us-west-2 is documented but the stack does not declare it.

lib/dynamodb-cross-account-replication-stack.ts only defines ReplicaAccountId. The replica region is the literal 'us-west-2'. Users following this command will either get a CloudFormation error ("Parameter ReplicaRegion does not exist in the template") or have the parameter silently ignored. They think they're choosing a region but they aren't. You may want to add the parameter to the stack and use it

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. TableV2 doesn't accept tokens for replica regions (throws ReplicaTableRegionCannotBeToken), so CfnParameter won't work here. Switched to CDK context (-c replicaRegion=us-west-2) which resolves at synth time. README updated to match.


## How it works

- A DynamoDB Global Table is created with a replica in the specified region

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use "Amazon DynamoDB" on first body reference. The title correctly uses "Amazon DynamoDB"; the first body paragraph and the "How it works" bullet immediately drop to "DynamoDB Global Table". Per AWS naming guidance the first body reference should re-introduce the full service name.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- first body reference now uses "Amazon DynamoDB Global Table".

- DynamoDB automatically replicates all writes to the replica with sub-second latency
- A cross-account IAM role allows the replica account to assume and read from the table
- Point-in-time recovery is enabled for data protection

## Testing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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)
aws dynamodb get-item --table-name <TableName> \
--key '{"PK":{"S":"user#123"},"SK":{"S":"profile"}}' \
--region us-west-2
```

## Cleanup

```
cdk destroy

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleanup must warn that cdk destroy deletes the replica and its data. RemovalPolicy.DESTROY plus cdk destroy deletes the source table and all replicas with no recovery. Worth a clear heads-up so testers don't lose data unwillingly. Do confirm this though.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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
12 changes: 12 additions & 0 deletions dynamodb-cross-account-replication-cdk/bin/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/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();
new DynamodbCrossAccountReplicationStack(app, 'DynamodbCrossAccountReplicationStack', {
env: {
account: process.env.CDK_DEFAULT_ACCOUNT || '123456789012',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded fallback account '123456789012' is misleading. Synthesizing against the docs-example account when CDK_DEFAULT_ACCOUNT is unset will produce templates that look fine in cdk synth but fail at deploy in confusing ways. Either fall back to environment-agnostic synthesis or fail loudly:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- app.ts now throws with a clear error message if CDK_DEFAULT_ACCOUNT is unset. No more silent fallback to a placeholder.

region: process.env.CDK_DEFAULT_REGION || 'us-east-1',
},
});
3 changes: 3 additions & 0 deletions dynamodb-cross-account-replication-cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"app": "npx ts-node --prefer-ts-exts bin/app.ts"
}
40 changes: 40 additions & 0 deletions dynamodb-cross-account-replication-cdk/example-pattern.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"title": "Amazon DynamoDB Cross-Account Replication with Global Tables",
"description": "Deploy a DynamoDB Global Table with cross-account replication and IAM role for secure cross-account read access.",
"language": "TypeScript",
"level": "300",
"framework": "CDK",
"introBox": {
"headline": "How it works",
"text": [
"This pattern creates a DynamoDB Global Table that replicates data across AWS accounts and regions.",
"A cross-account IAM role enables the replica account to read from the table securely.",
"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,52 @@
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 for the replica table',
});

const table = new dynamodb.TableV2(this, 'SourceTable', {

@parikhudit parikhudit Jun 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KMS expectations for cross-account reads are not addressed. TableV2 defaults to AWS-owned KMS keys, which work transparently across accounts. Real-world cross-account replication usually uses customer-managed KMS keys for compliance and at that point the replica account principal needs kms:Decrypt (and replication needs kms:GenerateDataKey/kms:Encrypt) on the key. Pattern docs should at minimum call this out so users don't get stuck on KMSAccessDeniedException after swapping in a CMK.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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(),
pointInTimeRecovery: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may want to refer and use pointInTimeRecoverySpecification instead, please check.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already using pointInTimeRecoverySpecification in the latest commit -- the deprecated pointInTimeRecovery was from an earlier version.

removalPolicy: cdk.RemovalPolicy.DESTROY,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 DESTROY is a foot-gun. Either flip the default to RETAIN, or keep DESTROY for testability and call it out clearly in source + README.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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: 'us-west-2',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Replica region 'us-west-2' is hardcoded in two places

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a CDK context value with us-west-2 as fallback (not a string literal baked into logic). Users override via -c replicaRegion=eu-west-1. The fallback is intentional so the pattern works out-of-the-box for quick testing.

},
],
});
Comment on lines +13 to +26

@parikhudit parikhudit Jun 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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.
TableV2.replicas only sets a region. There's no replica account property. The replica is created in the same AWS account as the source, in another region. True cross-account Global Tables require TableV2MultiAccountReplica in the replica account's stack, with replicaSourceTable referencing the source.

Possible ways to resolve:

  • Reframe as "cross-account READ over a same-account multi-region replica". Keep the current architecture, rename the pattern (dynamodb-cross-account-read-cdk?), and update title + README to make this explicit. The cross-account aspect is then only the IAM role.
  • Implement true multi-account Global Tables by spliting into two stacks

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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.


// IAM role for cross-account access to the replica
const crossAccountRole = new iam.Role(this, 'CrossAccountReplicaRole', {
assumedBy: new iam.AccountPrincipal(replicaAccountId.valueAsString),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trust policy allows any principal in the replica account; no sts:ExternalId or principal scoping.**

iam.AccountPrincipal(replicaAccountId) trusts the entire replica account. AWS recommends adding either an sts:ExternalId condition or a specific principal ARN to defend against confused-deputy patterns and to scope cross-account trust to a specific consumer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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 replica account to read from the global table replica',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dynamodb:Scan on a least-privilege read role. The role description is "Allows replica account to read from the global table replica." Scan reads every item broader than required for typical cross-account read use cases and a budget hazard. Drop unless explicitly needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above -- Scan dropped.

});

crossAccountRole.addToPolicy(new iam.PolicyStatement({
actions: [

@parikhudit parikhudit Jun 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross-account read role cannot access the replica region
table.tableArn resolves to the source-region (us-east-1 by default) ARN. DynamoDB IAM resource ARNs are region-scoped arn:aws:dynamodb:<region>:<account>:table/<name>. When the replica account assumes this role and runs GetItem against the us-west-2 replica, IAM evaluates against the us-west-2 ARN, which is not in the policy → AccessDenied. The pattern's stated purpose (cross-account read from the replica) seems functionally broken.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right -- table.tableArn is source-region-scoped. Fixed by adding explicit replica region ARNs to the policy so the reader account can access the table in both regions.

'dynamodb:GetItem',
'dynamodb:Query',
'dynamodb:Scan',
'dynamodb:BatchGetItem',
],
Comment on lines +36 to +39

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dynamodb:Scan on a least-privilege read role. The role description is "Allows replica account to read from the global table replica." Scan reads every item broader than required for typical cross-account read use cases and a budget hazard. Drop unless explicitly needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The 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: [
table.tableArn,
`${table.tableArn}/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: 'us-west-2' });
}
}
16 changes: 16 additions & 0 deletions dynamodb-cross-account-replication-cdk/package.json
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"
}
}
8 changes: 8 additions & 0 deletions dynamodb-cross-account-replication-cdk/tsconfig.json
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"]
}