Skip to content

Commit

Permalink
feat(events): retry-policy support (#13660)
Browse files Browse the repository at this point in the history
Add retry policy (+ dead letter queue) support for the following targets:
 
- Lambda
- LogGroup
- CodeBuild
- CodePipeline
- StepFunction

Closes #13659 

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
DaWyz authored Mar 24, 2021
1 parent 1f01b8a commit 7966f8d
Show file tree
Hide file tree
Showing 23 changed files with 469 additions and 65 deletions.
41 changes: 34 additions & 7 deletions packages/@aws-cdk/aws-events-targets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,28 @@ to the `rule.addTarget()` method.

Currently supported are:

* Start a CodeBuild build
* Start a CodePipeline pipeline
* [Start a CodeBuild build](#start-a-codebuild-build)
* [Start a CodePipeline pipeline](#start-a-codepipeline-pipeline)
* Run an ECS task
* Invoke a Lambda function
* [Invoke a Lambda function](#invoke-a-lambda-function)
* Publish a message to an SNS topic
* Send a message to an SQS queue
* Start a StepFunctions state machine
* [Start a StepFunctions state machine](#start-a-stepfunctions-state-machine)
* Queue a Batch job
* Make an AWS API call
* Put a record to a Kinesis stream
* Log an event into a LogGroup
* [Log an event into a LogGroup](#log-an-event-into-a-loggroup)
* Put a record to a Kinesis Data Firehose stream
* Put an event on an EventBridge bus

See the README of the `@aws-cdk/aws-events` library for more information on
EventBridge.

## Event retry policy and using dead-letter queues

The Codebuild, CodePipeline, Lambda, StepFunctions and LogGroup targets support attaching a [dead letter queue and setting retry policies](https://docs.aws.amazon.com/eventbridge/latest/userguide/rule-dlq.html). See the [lambda example](#invoke-a-lambda-function).
Use [escape hatches](https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html) for the other target types.

## Invoke a Lambda function

Use the `LambdaFunction` target to invoke a lambda function.
Expand All @@ -45,6 +50,7 @@ import * as lambda from "@aws-cdk/aws-lambda";
import * as events from "@aws-cdk/aws-events";
import * as sqs from "@aws-cdk/aws-sqs";
import * as targets from "@aws-cdk/aws-events-targets";
import * as cdk from '@aws-cdk/core';

const fn = new lambda.Function(this, 'MyFunc', {
runtime: lambda.Runtime.NODEJS_12_X,
Expand All @@ -62,6 +68,8 @@ const queue = new sqs.Queue(this, 'Queue');

rule.addTarget(new targets.LambdaFunction(fn, {
deadLetterQueue: queue, // Optional: add a dead letter queue
maxEventAge: cdk.Duration.hours(2), // Otional: set the maxEventAge retry policy
retryAttempts: 2, // Optional: set the max number of retry attempts
}));
```

Expand Down Expand Up @@ -90,7 +98,7 @@ const rule = new events.Rule(this, 'rule', {
rule.addTarget(new targets.CloudWatchLogGroup(logGroup));
```

## Trigger a CodeBuild project
## Start a CodeBuild build

Use the `CodeBuildProject` target to trigger a CodeBuild project.

Expand Down Expand Up @@ -123,7 +131,26 @@ const onCommitRule = repo.onCommit('OnCommit', {
});
```

## Trigger a State Machine
## Start a CodePipeline pipeline

Use the `CodePipeline` target to trigger a CodePipeline pipeline.

The code snippet below creates a CodePipeline pipeline that is triggered every hour

```ts
import * as codepipeline from '@aws-sdk/aws-codepipeline';
import * as sqs from '@aws-sdk/aws-sqs';

const pipeline = new codepipeline.Pipeline(this, 'Pipeline');

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

rule.addTarget(new targets.CodePipeline(pipeline));
```

## Start a StepFunctions state machine

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

Expand Down
19 changes: 3 additions & 16 deletions packages/@aws-cdk/aws-events-targets/lib/codebuild.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import * as codebuild from '@aws-cdk/aws-codebuild';
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 { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util';
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* Customize the CodeBuild Event Target
*/
export interface CodeBuildProjectProps {
export interface CodeBuildProjectProps extends TargetBaseProps {

/**
* The role to assume before invoking the target
Expand All @@ -25,18 +24,6 @@ export interface CodeBuildProjectProps {
* @default - the entire EventBridge event
*/
readonly event?: events.RuleTargetInput;

/**
* 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 @@ -58,8 +45,8 @@ export class CodeBuildProject implements events.IRuleTarget {
}

return {
...bindBaseTargetConfig(this.props),
arn: this.project.projectArn,
deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined,
role: this.props.eventRole || singletonEventRole(this.project, [
new iam.PolicyStatement({
actions: ['codebuild:StartBuild'],
Expand Down
6 changes: 4 additions & 2 deletions packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import { singletonEventRole } from './util';
import { bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* Customization options when creating a {@link CodePipeline} event target.
*/
export interface CodePipelineTargetOptions {
export interface CodePipelineTargetOptions extends TargetBaseProps {
/**
* The role to assume before invoking the target
* (i.e., the pipeline) when the given rule is triggered.
Expand All @@ -27,6 +27,8 @@ export class CodePipeline implements events.IRuleTarget {

public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
return {
...bindBaseTargetConfig(this.options),
id: '',
arn: this.pipeline.pipelineArn,
role: this.options.eventRole || singletonEventRole(this.pipeline, [new iam.PolicyStatement({
resources: [this.pipeline.pipelineArn],
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-events-targets/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export * from './state-machine';
export * from './kinesis-stream';
export * from './log-group';
export * from './kinesis-firehose-stream';
export * from './util';
19 changes: 3 additions & 16 deletions packages/@aws-cdk/aws-events-targets/lib/lambda.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as events from '@aws-cdk/aws-events';
import * as lambda from '@aws-cdk/aws-lambda';
import * as sqs from '@aws-cdk/aws-sqs';
import { addLambdaPermission, addToDeadLetterQueueResourcePolicy } from './util';
import { addLambdaPermission, addToDeadLetterQueueResourcePolicy, TargetBaseProps, bindBaseTargetConfig } from './util';

/**
* Customize the Lambda Event Target
*/
export interface LambdaFunctionProps {
export interface LambdaFunctionProps extends TargetBaseProps {
/**
* The event to send to the Lambda
*
Expand All @@ -15,18 +14,6 @@ export interface LambdaFunctionProps {
* @default the entire EventBridge event
*/
readonly event?: events.RuleTargetInput;

/**
* 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 @@ -50,8 +37,8 @@ export class LambdaFunction implements events.IRuleTarget {
}

return {
...bindBaseTargetConfig(this.props),
arn: this.handler.functionArn,
deadLetterConfig: this.props.deadLetterQueue ? { arn: this.props.deadLetterQueue?.queueArn } : undefined,
input: this.props.event,
targetResource: this.handler,
};
Expand Down
4 changes: 3 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/log-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import * as iam from '@aws-cdk/aws-iam';
import * as logs from '@aws-cdk/aws-logs';
import * as cdk from '@aws-cdk/core';
import { LogGroupResourcePolicy } from './log-group-resource-policy';
import { TargetBaseProps, bindBaseTargetConfig } from './util';

/**
* Customize the CloudWatch LogGroup Event Target
*/
export interface LogGroupProps {
export interface LogGroupProps extends TargetBaseProps {
/**
* The event to send to the CloudWatch LogGroup
*
Expand Down Expand Up @@ -45,6 +46,7 @@ export class CloudWatchLogGroup implements events.IRuleTarget {
}

return {
...bindBaseTargetConfig(this.props),
arn: logGroupStack.formatArn({
service: 'logs',
resource: 'log-group',
Expand Down
19 changes: 3 additions & 16 deletions packages/@aws-cdk/aws-events-targets/lib/state-machine.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
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 { addToDeadLetterQueueResourcePolicy, singletonEventRole } from './util';
import { addToDeadLetterQueueResourcePolicy, bindBaseTargetConfig, singletonEventRole, TargetBaseProps } from './util';

/**
* Customize the Step Functions State Machine target
*/
export interface SfnStateMachineProps {
export interface SfnStateMachineProps extends TargetBaseProps {
/**
* The input to the state machine execution
*
Expand All @@ -21,18 +20,6 @@ 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 Down Expand Up @@ -61,8 +48,8 @@ export class SfnStateMachine implements events.IRuleTarget {
}

return {
...bindBaseTargetConfig(this.props),
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
62 changes: 61 additions & 1 deletion packages/@aws-cdk/aws-events-targets/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,74 @@ import * as events from '@aws-cdk/aws-events';
import * as iam from '@aws-cdk/aws-iam';
import * as lambda from '@aws-cdk/aws-lambda';
import * as sqs from '@aws-cdk/aws-sqs';
import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison } from '@aws-cdk/core';
import { Annotations, ConstructNode, IConstruct, Names, Token, TokenComparison, Duration } from '@aws-cdk/core';

// keep this import separate from other imports to reduce chance for merge conflicts with v2-main
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct } from '@aws-cdk/core';

/**
* The generic properties for an RuleTarget
*/
export interface TargetBaseProps {
/**
* 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;
/**
* The maximum age of a request that Lambda sends to a function for
* processing.
*
* Minimum value of 60.
* Maximum value of 86400.
*
* @default Duration.hours(24)
*/
readonly maxEventAge?: Duration;

/**
* The maximum number of times to retry when the function returns an error.
*
* Minimum value of 0.
* Maximum value of 185.
*
* @default 185
*/
readonly retryAttempts?: number;
}

/**
* Bind props to base rule target config.
* @internal
*/
export function bindBaseTargetConfig(props: TargetBaseProps) {
let { deadLetterQueue, retryAttempts, maxEventAge } = props;

return {
deadLetterConfig: deadLetterQueue ? { arn: deadLetterQueue?.queueArn } : undefined,
retryPolicy: retryAttempts || maxEventAge
? {
maximumRetryAttempts: retryAttempts,
maximumEventAgeInSeconds: maxEventAge?.toSeconds({ integral: true }),
}
: undefined,
};
}


/**
* Obtain the Role for the EventBridge event
*
* If a role already exists, it will be returned. This ensures that if multiple
* events have the same target, they will share a role.
* @internal
*/
export function singletonEventRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole {
const id = 'EventsRole';
Expand All @@ -30,6 +87,7 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli

/**
* Allows a Lambda function to be called from a rule
* @internal
*/
export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunction): void {
let scope: Construct | undefined;
Expand All @@ -54,6 +112,7 @@ export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunctio

/**
* Allow a rule to send events with failed invocation to an Amazon SQS queue.
* @internal
*/
export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sqs.IQueue) {
if (!sameEnvDimension(rule.env.region, queue.env.region)) {
Expand Down Expand Up @@ -89,6 +148,7 @@ export function addToDeadLetterQueueResourcePolicy(rule: events.IRule, queue: sq
*
* Used to compare either accounts or regions, and also returns true if both
* are unresolved (in which case both are expted to be "current region" or "current account").
* @internal
*/
function sameEnvDimension(dim1: string, dim2: string) {
return [TokenComparison.SAME, TokenComparison.BOTH_UNRESOLVED].includes(Token.compareStrings(dim1, dim2));
Expand Down
Loading

0 comments on commit 7966f8d

Please sign in to comment.