Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
98 changes: 98 additions & 0 deletions dynamodb-cross-account-replication-cdk/README.md
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
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 reader account ID and replica region:
```bash
cdk deploy --parameters ReplicaAccountId=123456789012 -c replicaRegion=us-west-2
```

## 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, 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

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
17 changes: 17 additions & 0 deletions dynamodb-cross-account-replication-cdk/bin/app.ts
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 },
});
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 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', {

@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(),
pointInTimeRecoverySpecification: { pointInTimeRecoveryEnabled: true },
// DESTROY for easy cleanup in sample patterns. Use RETAIN in production.
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: replicaRegion }],
});
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.


// 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),

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

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: [
// 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 });
}
}
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"]
}