diff --git a/dynamodb-cross-account-replication-cdk/README.md b/dynamodb-cross-account-replication-cdk/README.md new file mode 100644 index 000000000..814518a96 --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/README.md @@ -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 + +```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 \ + --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 \ + --role-session-name reader-test + +# Export the temporary credentials +export AWS_ACCESS_KEY_ID= +export AWS_SECRET_ACCESS_KEY= +export AWS_SESSION_TOKEN= + +# Read from the replica region using the cross-account role +aws dynamodb get-item --table-name \ + --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 +``` + +--- + +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/dynamodb-cross-account-replication-cdk/bin/app.ts b/dynamodb-cross-account-replication-cdk/bin/app.ts new file mode 100644 index 000000000..35f7a3dc9 --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/bin/app.ts @@ -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 }, +}); diff --git a/dynamodb-cross-account-replication-cdk/cdk.json b/dynamodb-cross-account-replication-cdk/cdk.json new file mode 100644 index 000000000..a6700a2ff --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/cdk.json @@ -0,0 +1,3 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts" +} diff --git a/dynamodb-cross-account-replication-cdk/example-pattern.json b/dynamodb-cross-account-replication-cdk/example-pattern.json new file mode 100644 index 000000000..f74e23da1 --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/example-pattern.json @@ -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" + } + ] +} diff --git a/dynamodb-cross-account-replication-cdk/lib/dynamodb-cross-account-replication-stack.ts b/dynamodb-cross-account-replication-cdk/lib/dynamodb-cross-account-replication-stack.ts new file mode 100644 index 000000000..1466df750 --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/lib/dynamodb-cross-account-replication-stack.ts @@ -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', { + 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, + replicas: [{ region: replicaRegion }], + }); + + // 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), + 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', + ], + 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 }); + } +} diff --git a/dynamodb-cross-account-replication-cdk/package.json b/dynamodb-cross-account-replication-cdk/package.json new file mode 100644 index 000000000..dab05a14e --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/package.json @@ -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" + } +} diff --git a/dynamodb-cross-account-replication-cdk/tsconfig.json b/dynamodb-cross-account-replication-cdk/tsconfig.json new file mode 100644 index 000000000..a4f77b1b2 --- /dev/null +++ b/dynamodb-cross-account-replication-cdk/tsconfig.json @@ -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"] +}