diff --git a/packages/@aws-cdk/aws-dynamodb/README.md b/packages/@aws-cdk/aws-dynamodb/README.md index acd3f1820c156..ac540dd11670c 100644 --- a/packages/@aws-cdk/aws-dynamodb/README.md +++ b/packages/@aws-cdk/aws-dynamodb/README.md @@ -109,6 +109,17 @@ globalTable.autoScaleWriteCapacity({ }).scaleOnUtilization({ targetUtilizationPercent: 75 }); ``` +When adding a replica region for a large table, you might want to increase the +timeout for the replication operation: + +```ts +const globalTable = new dynamodb.Table(this, 'Table', { + partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING }, + replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'], + replicationTimeout: Duration.hours(2), // defaults to Duration.minutes(30) +}); +``` + ## Encryption All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table: diff --git a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts index 718c0e693e454..d984c4bb7b3ff 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts @@ -9,14 +9,26 @@ import { Construct } from 'constructs'; // eslint-disable-next-line no-duplicate-imports, import/order import { Construct as CoreConstruct } from '@aws-cdk/core'; +/** + * Properties for a ReplicaProvider + */ +export interface ReplicaProviderProps { + /** + * The timeout for the replication operation. + * + * @default Duration.minutes(30) + */ + readonly timeout?: Duration; +} + export class ReplicaProvider extends NestedStack { /** * Creates a stack-singleton resource provider nested stack. */ - public static getOrCreate(scope: Construct) { + public static getOrCreate(scope: Construct, props: ReplicaProviderProps = {}) { const stack = Stack.of(scope); const uid = '@aws-cdk/aws-dynamodb.ReplicaProvider'; - return stack.node.tryFindChild(uid) as ReplicaProvider || new ReplicaProvider(stack, uid); + return stack.node.tryFindChild(uid) as ReplicaProvider ?? new ReplicaProvider(stack, uid, props); } /** @@ -34,7 +46,7 @@ export class ReplicaProvider extends NestedStack { */ public readonly isCompleteHandler: lambda.Function; - private constructor(scope: Construct, id: string) { + private constructor(scope: Construct, id: string, props: ReplicaProviderProps = {}) { super(scope as CoreConstruct, id); const code = lambda.Code.fromAsset(path.join(__dirname, 'replica-handler')); @@ -80,6 +92,7 @@ export class ReplicaProvider extends NestedStack { onEventHandler: this.onEventHandler, isCompleteHandler: this.isCompleteHandler, queryInterval: Duration.seconds(10), + totalTimeout: props.timeout, }); } } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 8f36894fc5df8..9334192f5610d 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -3,8 +3,8 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { - Aws, CfnCondition, CfnCustomResource, CustomResource, Fn, - IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, + Aws, CfnCondition, CfnCustomResource, CustomResource, Duration, + Fn, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { DynamoDBMetrics } from './dynamodb-canned-metrics.generated'; @@ -218,6 +218,13 @@ export interface TableOptions { * @experimental */ readonly replicationRegions?: string[]; + + /** + * The timeout for a table replication operation in a single region. + * + * @default Duration.minutes(30) + */ + readonly replicationTimeout?: Duration; } /** @@ -1135,7 +1142,7 @@ export class Table extends TableBase { } if (props.replicationRegions && props.replicationRegions.length > 0) { - this.createReplicaTables(props.replicationRegions); + this.createReplicaTables(props.replicationRegions, props.replicationTimeout); } } @@ -1451,14 +1458,14 @@ export class Table extends TableBase { * * @param regions regions where to create tables */ - private createReplicaTables(regions: string[]) { + private createReplicaTables(regions: string[], timeout?: Duration) { const stack = Stack.of(this); if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) { throw new Error('`replicationRegions` cannot include the region where this stack is deployed.'); } - const provider = ReplicaProvider.getOrCreate(this); + const provider = ReplicaProvider.getOrCreate(this, { timeout }); // Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html // is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 768094c253213..cddba2ff1504b 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -4,6 +4,7 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { App, Aws, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Resource, Stack, Tags } from '@aws-cdk/core'; +import * as cr from '@aws-cdk/custom-resources'; import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag'; import { Construct } from 'constructs'; import { @@ -20,6 +21,8 @@ import { CfnTable, } from '../lib'; +jest.mock('@aws-cdk/custom-resources'); + /* eslint-disable quote-props */ // CDK parameters @@ -2295,12 +2298,6 @@ describe('global', () => { // THEN expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2311,12 +2308,6 @@ describe('global', () => { expect(stack).toHaveResource('Custom::DynamoDBReplica', { Properties: { - ServiceToken: { - 'Fn::GetAtt': [ - 'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D', - 'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn', - ], - }, TableName: { Ref: 'TableCD117FA1', }, @@ -2814,6 +2805,26 @@ describe('global', () => { // THEN expect(SynthUtils.toCloudFormation(stack).Conditions).toBeUndefined(); }); + + test('can configure timeout', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new Table(stack, 'Table', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + replicationRegions: ['eu-central-1'], + replicationTimeout: Duration.hours(1), + }); + + // THEN + expect(cr.Provider).toHaveBeenCalledWith(expect.anything(), expect.any(String), expect.objectContaining({ + totalTimeout: Duration.hours(1), + })); + }); }); test('L1 inside L2 expects removalpolicy to have been set', () => {