Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(events): retry-policy support #13660

Merged
merged 6 commits into from
Mar 24, 2021
Merged
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
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