From 1b134a58c1ce3afb14bd065b636762d3a4b20cf6 Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Thu, 6 Sep 2018 00:30:37 -0700 Subject: [PATCH] feat(aws-lambda): support dead letter queues (#663) Adds support for DLQ configuration in Lambda --- CHANGELOG.md | 1 + packages/@aws-cdk/aws-lambda/README.md | 15 + packages/@aws-cdk/aws-lambda/lib/lambda.ts | 41 ++ packages/@aws-cdk/aws-lambda/package.json | 1 + .../@aws-cdk/aws-lambda/test/test.lambda.ts | 589 +++++++++++++++++- 5 files changed, 646 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e43468e1f9126..9bfdf675fcacf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - AWS::Glue::Classifier JsonClassifier (__added__) - AWS::Glue::Classifier XMLClassifier (__added__) - AWS::Glue::Crawler Configuration (__added__) + - AWS::Lambda::Lambda DLQConfigurationSupport (__added__) - AWS::Neptune::DBInstance DBSubnetGroupName.UpdateType (__changed__) - Old: Mutable - New: Immutable diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 8956bf1af702c..9bfb3412ac218 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -60,3 +60,18 @@ new lambda.PipelineInvokeAction(this, 'Lambda', { See [the AWS documentation](https://docs.aws.amazon.com/codepipeline/latest/userguide/actions-invoke-lambda-function.html) on how to write a Lambda function invoked from CodePipeline. + +### Lambda with DLQ + +```ts +import lambda = require('@aws-cdk/aws-lambda'); + +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NodeJS810, + handler: 'index.handler' + code: lambda.Code.inline('exports.handler = function(event, ctx, cb) { return cb(null, "hi"); }'), + deadLetterQueueEnabled: true +}); +``` +See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/dlq.html) +to learn more about AWS Lambdas and DLQs. diff --git a/packages/@aws-cdk/aws-lambda/lib/lambda.ts b/packages/@aws-cdk/aws-lambda/lib/lambda.ts index adf5bf5434a1b..fa2c81b7412b1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/lambda.ts +++ b/packages/@aws-cdk/aws-lambda/lib/lambda.ts @@ -1,4 +1,5 @@ import iam = require('@aws-cdk/aws-iam'); +import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); import { Code } from './code'; import { FunctionRef } from './lambda-ref'; @@ -89,6 +90,22 @@ export interface FunctionProps { * Both supplied and generated roles can always be changed by calling `addToRolePolicy`. */ role?: iam.Role; + + /** + * Enabled DLQ. If `deadLetterQueue` is undefined, + * an SQS queue with default options will be defined for your Function. + * + * @default false unless `deadLetterQueue` is set, which implies DLQ is enabled + */ + deadLetterQueueEnabled?: boolean; + + /** + * The SQS queue to use if DLQ is enabled. + * + * @default SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true` + */ + deadLetterQueue?: sqs.QueueRef; + } /** @@ -166,6 +183,7 @@ export class Function extends FunctionRef { role: this.role.roleArn, environment: new cdk.Token(() => this.renderEnvironment()), memorySize: props.memorySize, + deadLetterConfig: this.buildDeadLetterConfig(props), }); resource.addDependency(this.role); @@ -226,4 +244,27 @@ export class Function extends FunctionRef { variables: this.environment }; } + + private buildDeadLetterConfig(props: FunctionProps) { + if (props.deadLetterQueue && props.deadLetterQueueEnabled === false) { + throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false'); + } + + if (!props.deadLetterQueue && !props.deadLetterQueueEnabled) { + return undefined; + } + + const deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', { + retentionPeriodSec: 1209600 + }); + + this.addToRolePolicy(new cdk.PolicyStatement() + .addAction('sqs:SendMessage') + .addResource(deadLetterQueue.queueArn)); + + return { + targetArn: deadLetterQueue.queueArn + }; + } + } diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index a4cee9cbd8c36..dd989ee5b0882 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -64,6 +64,7 @@ "@aws-cdk/aws-logs": "^0.8.2", "@aws-cdk/aws-s3": "^0.8.2", "@aws-cdk/aws-s3-notifications": "^0.8.2", + "@aws-cdk/aws-sqs": "^0.8.2", "@aws-cdk/cdk": "^0.8.2", "@aws-cdk/cx-api": "^0.8.2" }, diff --git a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts index 117049e09960a..c30e8fa9e1c43 100644 --- a/packages/@aws-cdk/aws-lambda/test/test.lambda.ts +++ b/packages/@aws-cdk/aws-lambda/test/test.lambda.ts @@ -1,6 +1,7 @@ import { countResources, expect, haveResource } from '@aws-cdk/assert'; import events = require('@aws-cdk/aws-events'); import iam = require('@aws-cdk/aws-iam'); +import sqs = require('@aws-cdk/aws-sqs'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import path = require('path'); @@ -323,7 +324,593 @@ export = { })); test.done(); - } + }, + + 'default function with SQS DLQ when client sets deadLetterQueueEnabled to true and functionName defined by client'(test: Test) { + const stack = new cdk.Stack(); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + functionName: 'OneFunctionToRuleThemAll', + deadLetterQueueEnabled: true + }); + + expect(stack).toMatch( + { + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyLambdaDeadLetterQueue399EEA2D", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + } + }, + "MyLambdaDeadLetterQueue399EEA2D": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600 + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "MyLambdaDeadLetterQueue399EEA2D", + "Arn" + ] + } + }, + "FunctionName": "OneFunctionToRuleThemAll" + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68" + ] + } + } + } + ); + test.done(); + }, + + 'default function with SQS DLQ when client sets deadLetterQueueEnabled to true and functionName not defined by client'(test: Test) { + const stack = new cdk.Stack(); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + deadLetterQueueEnabled: true, + }); + + expect(stack).toMatch( + { + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyLambdaDeadLetterQueue399EEA2D", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + } + }, + "MyLambdaDeadLetterQueue399EEA2D": { + "Type": "AWS::SQS::Queue", + "Properties": { + "MessageRetentionPeriod": 1209600 + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "MyLambdaDeadLetterQueue399EEA2D", + "Arn" + ] + } + } + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68" + ] + } + } + } + ); + test.done(); + }, + + 'default function with SQS DLQ when client sets deadLetterQueueEnabled to false'(test: Test) { + const stack = new cdk.Stack(); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + deadLetterQueueEnabled: false, + }); + + expect(stack).toMatch( + { + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "nodejs6.10" + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6" + ] + } + } + } + ); + test.done(); + }, + + 'default function with SQS DLQ when client provides Queue to be used as DLQ'(test: Test) { + const stack = new cdk.Stack(); + + const dlqStack = new cdk.Stack(); + + const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + queueName: 'MyLambda_DLQ', + retentionPeriodSec: 1209600 + }); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + deadLetterQueue: dlQueue, + }); + + expect(stack).toMatch( + { + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68" + ] + } + } + } + ); + test.done(); + }, + + 'default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to true'(test: Test) { + const stack = new cdk.Stack(); + + const dlqStack = new cdk.Stack(); + + const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + queueName: 'MyLambda_DLQ', + retentionPeriodSec: 1209600 + }); + + new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + deadLetterQueueEnabled: true, + deadLetterQueue: dlQueue, + }); + + expect(stack).toMatch( + { + "Resources": { + "MyLambdaServiceRole4539ECB6": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn", + ":", + { + "Ref": "AWS::Partition" + }, + ":", + "iam", + ":", + "", + ":", + "aws", + ":", + "policy", + "/", + "service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "MyLambdaServiceRoleDefaultPolicy5BBC6F68": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sqs:SendMessage", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyLambdaServiceRoleDefaultPolicy5BBC6F68", + "Roles": [ + { + "Ref": "MyLambdaServiceRole4539ECB6" + } + ] + } + }, + "MyLambdaCCE802FB": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "ZipFile": "foo" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyLambdaServiceRole4539ECB6", + "Arn" + ] + }, + "Runtime": "nodejs6.10", + "DeadLetterConfig": { + "TargetArn": { + "Fn::GetAtt": [ + "DeadLetterQueue9F481546", + "Arn" + ] + } + } + }, + "DependsOn": [ + "MyLambdaServiceRole4539ECB6", + "MyLambdaServiceRoleDefaultPolicy5BBC6F68" + ] + } + } + } + ); + test.done(); + }, + + 'error when default function with SQS DLQ when client provides Queue to be used as DLQ and deadLetterQueueEnabled set to false'(test: Test) { + const stack = new cdk.Stack(); + + const dlqStack = new cdk.Stack(); + + const dlQueue = new sqs.Queue(dlqStack, 'DeadLetterQueue', { + queueName: 'MyLambda_DLQ', + retentionPeriodSec: 1209600 + }); + + test.throws(() => new lambda.Function(stack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'index.handler', + runtime: lambda.Runtime.NodeJS610, + deadLetterQueueEnabled: false, + deadLetterQueue: dlQueue, + }), /deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false/); + + test.done(); + }, + }; function newTestLambda(parent: cdk.Construct) {