Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
mergify[bot] authored Mar 8, 2021
2 parents f3af204 + 56c2029 commit ce45999
Show file tree
Hide file tree
Showing 14 changed files with 411 additions and 80 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-docdb/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export class DatabaseCluster extends DatabaseClusterBase {
public readonly clusterResourceIdentifier: string;

/**
* The connections object to implement IConectable
* The connections object to implement IConnectable
*/
public readonly connections: ec2.Connections;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class GlobalTableCoordinator extends cdk.Stack {
code: lambda.Code.fromAsset(path.resolve(__dirname, '../', 'lambda-packages', 'aws-global-table-coordinator', 'lib')),
description: 'Lambda to make DynamoDB a global table',
handler: 'index.handler',
runtime: lambda.Runtime.NODEJS_10_X,
runtime: lambda.Runtime.NODEJS_14_X,
timeout: cdk.Duration.minutes(5),
uuid: 'D38B65A6-6B54-4FB6-9BAD-9CD40A6DAC12',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
"Arn"
]
},
"Runtime": "nodejs10.x",
"Runtime": "nodejs14.x",
"Description": "Lambda to make DynamoDB a global table",
"Timeout": 300
},
Expand Down
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 16 additions & 3 deletions packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand All @@ -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'));
Expand Down Expand Up @@ -80,6 +92,7 @@ export class ReplicaProvider extends NestedStack {
onEventHandler: this.onEventHandler,
isCompleteHandler: this.isCompleteHandler,
queryInterval: Duration.seconds(10),
totalTimeout: props.timeout,
});
}
}
17 changes: 12 additions & 5 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand Down
35 changes: 23 additions & 12 deletions packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -20,6 +21,8 @@ import {
CfnTable,
} from '../lib';

jest.mock('@aws-cdk/custom-resources');

/* eslint-disable quote-props */

// CDK parameters
Expand Down Expand Up @@ -2295,12 +2298,6 @@ describe('global', () => {
// THEN
expect(stack).toHaveResource('Custom::DynamoDBReplica', {
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D',
'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn',
],
},
TableName: {
Ref: 'TableCD117FA1',
},
Expand All @@ -2311,12 +2308,6 @@ describe('global', () => {

expect(stack).toHaveResource('Custom::DynamoDBReplica', {
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D',
'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn',
],
},
TableName: {
Ref: 'TableCD117FA1',
},
Expand Down Expand Up @@ -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', () => {
Expand Down
36 changes: 36 additions & 0 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,39 @@ const rule = new events.Rule(this, 'rule', {

rule.addTarget(new targets.CloudWatchLogGroup(logGroup));
```

## Trigger a State Machine

Use the `SfnStateMachine` target to trigger a State Machine.

The code snippet below creates a Simple StateMachine that is triggered every minute with a
dummy object as input.
You can optionally attach a
[dead letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html)
to the target.

```ts
import * as iam from '@aws-sdk/aws-iam';
import * as sqs from '@aws-sdk/aws-sqs';
import * as sfn from '@aws-cdk/aws-stepfunctions';
import * as targets from "@aws-cdk/aws-events-targets";

const rule = new events.Rule(stack, 'Rule', {
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
});

const dlq = new sqs.Queue(stack, 'DeadLetterQueue');

const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
});
const stateMachine = new sfn.StateMachine(stack, 'SM', {
definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }),
role,
});

rule.addTarget(new targets.SfnStateMachine(stateMachine, {
input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }),
deadLetterQueue: dlq,
}));
```
20 changes: 19 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/state-machine.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as sqs from '@aws-cdk/aws-sqs';
import * as sfn from '@aws-cdk/aws-stepfunctions';
import { singletonEventRole } from './util';
import { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util';

/**
* Customize the Step Functions State Machine target
Expand All @@ -20,6 +21,18 @@ export interface SfnStateMachineProps {
* @default - a new role will be created
*/
readonly role?: iam.IRole;

/**
* The SQS queue to be used as deadLetterQueue.
* Check out the [considerations for using a dead-letter queue](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html#dlq-considerations).
*
* The events not successfully delivered are automatically retried for a specified period of time,
* depending on the retry policy of the target.
* If an event is not delivered before all retry attempts are exhausted, it will be sent to the dead letter queue.
*
* @default - no dead-letter queue
*/
readonly deadLetterQueue?: sqs.IQueue;
}

/**
Expand All @@ -43,9 +56,14 @@ export class SfnStateMachine implements events.IRuleTarget {
* @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html#sns-permissions
*/
public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
if (this.props.deadLetterQueue) {
addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue);
}

return {
id: '',
arn: this.machine.stateMachineArn,
deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined,
role: this.role,
input: this.props.input,
targetResource: this.machine,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '@aws-cdk/assert/jest';
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as sqs from '@aws-cdk/aws-sqs';
import * as sfn from '@aws-cdk/aws-stepfunctions';
import * as cdk from '@aws-cdk/core';
import * as targets from '../../lib';
Expand Down Expand Up @@ -110,3 +111,95 @@ test('Existing role can be used for State machine Rule target', () => {
},
});
});

test('use a Dead Letter Queue for the rule target', () => {
// GIVEN
const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');

const rule = new events.Rule(stack, 'Rule', {
schedule: events.Schedule.rate(cdk.Duration.minutes(1)),
});

const dlq = new sqs.Queue(stack, 'DeadLetterQueue');

const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
});
const stateMachine = new sfn.StateMachine(stack, 'SM', {
definition: new sfn.Wait(stack, 'Hello', { time: sfn.WaitTime.duration(cdk.Duration.seconds(10)) }),
role,
});

// WHEN
rule.addTarget(new targets.SfnStateMachine(stateMachine, {
input: events.RuleTargetInput.fromObject({ SomeParam: 'SomeValue' }),
deadLetterQueue: dlq,
}));

// the Permission resource should be in the event stack
expect(stack).toHaveResource('AWS::Events::Rule', {
ScheduleExpression: 'rate(1 minute)',
State: 'ENABLED',
Targets: [
{
Arn: {
Ref: 'SM934E715A',
},
DeadLetterConfig: {
Arn: {
'Fn::GetAtt': [
'DeadLetterQueue9F481546',
'Arn',
],
},
},
Id: 'Target0',
Input: '{"SomeParam":"SomeValue"}',
RoleArn: {
'Fn::GetAtt': [
'SMEventsRoleB320A902',
'Arn',
],
},
},
],
});

expect(stack).toHaveResource('AWS::SQS::QueuePolicy', {
PolicyDocument: {
Statement: [
{
Action: 'sqs:SendMessage',
Condition: {
ArnEquals: {
'aws:SourceArn': {
'Fn::GetAtt': [
'Rule4C995B7F',
'Arn',
],
},
},
},
Effect: 'Allow',
Principal: {
Service: 'events.amazonaws.com',
},
Resource: {
'Fn::GetAtt': [
'DeadLetterQueue9F481546',
'Arn',
],
},
Sid: 'AllowEventRuleStackRuleF6E31DD0',
},
],
Version: '2012-10-17',
},
Queues: [
{
Ref: 'DeadLetterQueue9F481546',
},
],
});
});
Loading

0 comments on commit ce45999

Please sign in to comment.