From 7966f8d48c4bff26beb22856d289f9d0c7e7081d Mon Sep 17 00:00:00 2001 From: Alban Esc Date: Wed, 24 Mar 2021 01:59:05 -0700 Subject: [PATCH] feat(events): retry-policy support (#13660) 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* --- .../@aws-cdk/aws-events-targets/README.md | 41 ++++++++-- .../aws-events-targets/lib/codebuild.ts | 19 +---- .../aws-events-targets/lib/codepipeline.ts | 6 +- .../@aws-cdk/aws-events-targets/lib/index.ts | 1 + .../@aws-cdk/aws-events-targets/lib/lambda.ts | 19 +---- .../aws-events-targets/lib/log-group.ts | 4 +- .../aws-events-targets/lib/state-machine.ts | 19 +---- .../@aws-cdk/aws-events-targets/lib/util.ts | 62 ++++++++++++++- .../test/codebuild/codebuild.test.ts | 50 +++++++++++- .../integ.project-events.expected.json | 4 + .../test/codebuild/integ.project-events.ts | 2 + .../integ.pipeline-event-target.expected.json | 17 +++++ .../integ.pipeline-event-target.ts | 9 ++- .../test/codepipeline/pipeline.test.ts | 68 ++++++++++++++++- .../test/lambda/integ.events.expected.json | 6 +- .../test/lambda/integ.events.ts | 2 + .../test/lambda/lambda.test.ts | 46 +++++++++++ .../test/logs/integ.log-group.expected.json | 19 ++++- .../test/logs/integ.log-group.ts | 9 ++- .../test/logs/log-group.test.ts | 76 +++++++++++++++++++ .../test/stepfunctions/statemachine.test.ts | 48 ++++++++++++ packages/@aws-cdk/aws-events/lib/rule.ts | 1 + packages/@aws-cdk/aws-events/lib/target.ts | 6 ++ 23 files changed, 469 insertions(+), 65 deletions(-) diff --git a/packages/@aws-cdk/aws-events-targets/README.md b/packages/@aws-cdk/aws-events-targets/README.md index d49621b0199cd..e75e999c1a83a 100644 --- a/packages/@aws-cdk/aws-events-targets/README.md +++ b/packages/@aws-cdk/aws-events-targets/README.md @@ -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. @@ -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, @@ -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 })); ``` @@ -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. @@ -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. diff --git a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts index a5264b2abe378..a9da8719cfef7 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codebuild.ts @@ -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 @@ -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; } /** @@ -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'], diff --git a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts index fc0eb095cfecd..8d2006378f121 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts @@ -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. @@ -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], diff --git a/packages/@aws-cdk/aws-events-targets/lib/index.ts b/packages/@aws-cdk/aws-events-targets/lib/index.ts index 155791c195d1e..674e657d640a4 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/index.ts @@ -12,3 +12,4 @@ export * from './state-machine'; export * from './kinesis-stream'; export * from './log-group'; export * from './kinesis-firehose-stream'; +export * from './util'; diff --git a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts index 903b3a2f33683..8dd455ce51056 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/lambda.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/lambda.ts @@ -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 * @@ -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; } /** @@ -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, }; diff --git a/packages/@aws-cdk/aws-events-targets/lib/log-group.ts b/packages/@aws-cdk/aws-events-targets/lib/log-group.ts index d437adaf2375c..688cfc3773c13 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/log-group.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/log-group.ts @@ -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 * @@ -45,6 +46,7 @@ export class CloudWatchLogGroup implements events.IRuleTarget { } return { + ...bindBaseTargetConfig(this.props), arn: logGroupStack.formatArn({ service: 'logs', resource: 'log-group', diff --git a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts index 2407a2023c96b..ce780bf99d2d2 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/state-machine.ts @@ -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 * @@ -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; } /** @@ -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, diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index 069b04a8c5131..6805b7245000f 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -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'; @@ -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; @@ -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)) { @@ -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)); diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts index 1eba641d7c8ce..235ac1da203bc 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/codebuild.test.ts @@ -3,7 +3,7 @@ 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 { CfnElement, Stack } from '@aws-cdk/core'; +import { CfnElement, Duration, Stack } from '@aws-cdk/core'; import * as targets from '../../lib'; describe('CodeBuild event target', () => { @@ -122,6 +122,54 @@ describe('CodeBuild event target', () => { })); }); + test('specifying retry policy', () => { + // GIVEN + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + // WHEN + const eventInput = { + buildspecOverride: 'buildspecs/hourly.yml', + }; + + rule.addTarget( + new targets.CodeBuildProject(project, { + event: events.RuleTargetInput.fromObject(eventInput), + retryAttempts: 2, + maxEventAge: Duration.hours(2), + }), + ); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyProject39F7B0AE', + 'Arn', + ], + }, + Id: 'Target0', + Input: '{"buildspecOverride":"buildspecs/hourly.yml"}', + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'MyProjectEventsRole5B7D93F5', + 'Arn', + ], + }, + }, + ], + })); + }); + test('use a Dead Letter Queue for the rule target', () => { // GIVEN const rule = new events.Rule(stack, 'Rule', { diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index aec41898a4622..6d65b68ccb033 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -51,6 +51,10 @@ ] } }, + "RetryPolicy": { + "MaximumEventAgeInSeconds": 7200, + "MaximumRetryAttempts": 2 + }, "Id": "Target0", "RoleArn": { "Fn::GetAtt": [ diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts index 7974a4188f969..83b48805d2387 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.ts @@ -42,6 +42,8 @@ project.onPhaseChange('PhaseChange', { const onCommitRule = repo.onCommit('OnCommit', { target: new targets.CodeBuildProject(project, { deadLetterQueue: deadLetterQueue, + maxEventAge: cdk.Duration.hours(2), + retryAttempts: 2, }), branches: ['master'], }); diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json index 66418749eda33..7f2c9d48da34b 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.expected.json @@ -397,6 +397,11 @@ ] } }, + "dlq09C78ACC": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "ruleF2C1DCDC": { "Type": "AWS::Events::Rule", "Properties": { @@ -427,7 +432,19 @@ ] ] }, + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "dlq09C78ACC", + "Arn" + ] + } + }, "Id": "Target0", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 7200, + "MaximumRetryAttempts": 2 + }, "RoleArn": { "Fn::GetAtt": [ "pipelinePipeline22F2A91DEventsRole048D7F59", diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts index b4f321230234f..dafad82627605 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/integ.pipeline-event-target.ts @@ -1,6 +1,7 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as events from '@aws-cdk/aws-events'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as targets from '../../lib'; @@ -64,9 +65,15 @@ pipeline.addStage({ })], }); +let queue = new sqs.Queue(stack, 'dlq'); + new events.Rule(stack, 'rule', { schedule: events.Schedule.expression('rate(1 minute)'), - targets: [new targets.CodePipeline(pipeline)], + targets: [new targets.CodePipeline(pipeline, { + deadLetterQueue: queue, + maxEventAge: cdk.Duration.hours(2), + retryAttempts: 2, + })], }); app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts index 68ee1a10ae449..31730035a3c05 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/codepipeline/pipeline.test.ts @@ -2,7 +2,8 @@ import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; 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 { CfnElement, Stack } from '@aws-cdk/core'; +import * as sqs from '@aws-cdk/aws-sqs'; +import { CfnElement, Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as targets from '../../lib'; @@ -93,6 +94,71 @@ describe('CodePipeline event target', () => { }); }); + describe('with retry policy and dead letter queue', () => { + test('adds retry attempts and maxEventAge to the target configuration', () => { + // WHEN + let queue = new sqs.Queue(stack, 'dlq'); + + rule.addTarget(new targets.CodePipeline(pipeline, { + retryAttempts: 2, + maxEventAge: Duration.hours(2), + deadLetterQueue: queue, + })); + + // THEN + expect(stack).to(haveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':codepipeline:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':', + { + Ref: 'PipelineC660917D', + }, + ], + ], + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'dlq09C78ACC', + 'Arn', + ], + }, + }, + Id: 'Target0', + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'PipelineEventsRole46BEEA7C', + 'Arn', + ], + }, + }, + ], + })); + }); + }); + describe('with an explicit event role', () => { beforeEach(() => { const role = new iam.Role(stack, 'MyExampleRole', { diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json index 7df2a4becf021..32474ae439abf 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json @@ -145,7 +145,11 @@ ] } }, - "Id": "Target0" + "Id": "Target0", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 7200, + "MaximumRetryAttempts": 2 + } } ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts index c37c632d803ae..3991d56d9ef56 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.ts @@ -33,6 +33,8 @@ const queue = new sqs.Queue(stack, 'Queue'); timer3.addTarget(new targets.LambdaFunction(fn, { deadLetterQueue: queue, + maxEventAge: cdk.Duration.hours(2), + retryAttempts: 2, })); app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index c5fd260715293..a32b0f422c29b 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -325,6 +325,52 @@ test('must display a warning when using a Dead Letter Queue from another account expect(rule?.node.metadata[0].data).toMatch(/Cannot add a resource policy to your dead letter queue associated with rule .* because the queue is in a different account\. You must add the resource policy manually to the dead letter queue in account 222222222222\./); }); + +test('specifying retry policy', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + + const fn = new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + // WHEN + new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn, { + retryAttempts: 2, + maxEventAge: cdk.Duration.hours(2), + })], + }); + + // THEN + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::GetAtt': [ + 'MyLambdaCCE802FB', + 'Arn', + ], + }, + Id: 'Target0', + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + }, + ], + }); +}); + function newTestLambda(scope: constructs.Construct, suffix = '') { return new lambda.Function(scope, `MyLambda${suffix}`, { code: new lambda.InlineCode('foo'), diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json index 9a150d9cafa32..d39babefd566a 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.expected.json @@ -337,6 +337,11 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "dlq09C78ACC": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "Timer30894E3BB": { "Type": "AWS::Events::Rule", "Properties": { @@ -364,7 +369,19 @@ ] ] }, - "Id": "Target0" + "DeadLetterConfig": { + "Arn": { + "Fn::GetAtt": [ + "dlq09C78ACC", + "Arn" + ] + } + }, + "Id": "Target0", + "RetryPolicy": { + "MaximumEventAgeInSeconds": 7200, + "MaximumRetryAttempts": 2 + } } ] } diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts index 6d813dd3df3dd..df8fcfae9e8bd 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/integ.log-group.ts @@ -1,5 +1,6 @@ import * as events from '@aws-cdk/aws-events'; import * as logs from '@aws-cdk/aws-logs'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -38,10 +39,16 @@ timer2.addTarget(new targets.CloudWatchLogGroup(logGroup2, { }), })); +const queue = new sqs.Queue(stack, 'dlq'); + const timer3 = new events.Rule(stack, 'Timer3', { schedule: events.Schedule.rate(cdk.Duration.minutes(1)), }); -timer3.addTarget(new targets.CloudWatchLogGroup(importedLogGroup)); +timer3.addTarget(new targets.CloudWatchLogGroup(importedLogGroup, { + deadLetterQueue: queue, + maxEventAge: cdk.Duration.hours(2), + retryAttempts: 2, +})); app.synth(); diff --git a/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts b/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts index 6df5ad191770a..5af09c63de0d9 100644 --- a/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/logs/log-group.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as logs from '@aws-cdk/aws-logs'; +import * as sqs from '@aws-cdk/aws-sqs'; import * as cdk from '@aws-cdk/core'; import * as targets from '../../lib'; @@ -110,3 +111,78 @@ test('use log group as an event rule target with rule target input', () => { ], }); }); + +test('specifying retry policy and dead letter queue', () => { + // GIVEN + const stack = new cdk.Stack(); + const logGroup = new logs.LogGroup(stack, 'MyLogGroup', { + logGroupName: '/aws/events/MyLogGroup', + }); + const rule1 = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + }); + + const queue = new sqs.Queue(stack, 'Queue'); + + // WHEN + rule1.addTarget(new targets.CloudWatchLogGroup(logGroup, { + event: events.RuleTargetInput.fromObject({ + data: events.EventField.fromPath('$'), + }), + retryAttempts: 2, + maxEventAge: cdk.Duration.hours(2), + deadLetterQueue: queue, + })); + + // THEN + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 minute)', + State: 'ENABLED', + Targets: [ + { + Arn: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':logs:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':log-group:', + { + Ref: 'MyLogGroup5C0DAD85', + }, + ], + ], + }, + DeadLetterConfig: { + Arn: { + 'Fn::GetAtt': [ + 'Queue4A7E3555', + 'Arn', + ], + }, + }, + Id: 'Target0', + InputTransformer: { + InputPathsMap: { + f1: '$', + }, + InputTemplate: '{"data":}', + }, + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + }, + ], + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts index 9dc3b2d0a4e83..651bc7ff3f654 100644 --- a/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/stepfunctions/statemachine.test.ts @@ -112,6 +112,54 @@ test('Existing role can be used for State machine Rule target', () => { }); }); +test('specifying retry policy', () => { + // GIVEN + const stack = new cdk.Stack(); + const rule = new events.Rule(stack, 'Rule', { + schedule: events.Schedule.expression('rate(1 hour)'), + }); + + // WHEN + 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' }), + maxEventAge: cdk.Duration.hours(2), + retryAttempts: 2, + })); + + // THEN + expect(stack).toHaveResource('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { + Ref: 'SM934E715A', + }, + Id: 'Target0', + Input: '{"SomeParam":"SomeValue"}', + RetryPolicy: { + MaximumEventAgeInSeconds: 7200, + MaximumRetryAttempts: 2, + }, + RoleArn: { + 'Fn::GetAtt': [ + 'SMEventsRoleB320A902', + 'Arn', + ], + }, + }, + ], + }); +}); + test('use a Dead Letter Queue for the rule target', () => { // GIVEN const app = new cdk.App(); diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index 2c71054b60828..e6cbf6afd728c 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -296,6 +296,7 @@ export class Rule extends Resource implements IRule { runCommandParameters: targetProps.runCommandParameters, batchParameters: targetProps.batchParameters, deadLetterConfig: targetProps.deadLetterConfig, + retryPolicy: targetProps.retryPolicy, sqsParameters: targetProps.sqsParameters, input: inputProps && inputProps.input, inputPath: inputProps && inputProps.inputPath, diff --git a/packages/@aws-cdk/aws-events/lib/target.ts b/packages/@aws-cdk/aws-events/lib/target.ts index 1f7de7e82b36a..6b880e0afa4d5 100644 --- a/packages/@aws-cdk/aws-events/lib/target.ts +++ b/packages/@aws-cdk/aws-events/lib/target.ts @@ -54,6 +54,12 @@ export interface RuleTargetConfig { */ readonly deadLetterConfig?: CfnRule.DeadLetterConfigProperty; + /** + * A RetryPolicy object that includes information about the retry policy settings. + * @default EventBridge default retry policy + */ + readonly retryPolicy?: CfnRule.RetryPolicyProperty; + /** * The Amazon ECS task definition and task count to use, if the event target * is an Amazon ECS task.