Skip to content

Commit f8d8fe4

Browse files
feat(lambda): allow Topic to be dlq for Lambda (#18546)
Adds possibility of using sns.Topic as a deadLetterQueue for a lambda function as described in `AWS::Lambda::Function` documentation. closes: #16246 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent bc47b29 commit f8d8fe4

File tree

4 files changed

+191
-26
lines changed

4 files changed

+191
-26
lines changed

packages/@aws-cdk/aws-lambda/README.md

+16-1
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,8 @@ const fn = lambda.Function.fromFunctionAttributes(this, 'Function', {
512512
## Lambda with DLQ
513513

514514
A dead-letter queue can be automatically created for a Lambda function by
515-
setting the `deadLetterQueueEnabled: true` configuration.
515+
setting the `deadLetterQueueEnabled: true` configuration. In such case CDK creates
516+
a `sqs.Queue` as `deadLetterQueue`.
516517

517518
```ts
518519
const fn = new lambda.Function(this, 'MyFunction', {
@@ -537,6 +538,20 @@ const fn = new lambda.Function(this, 'MyFunction', {
537538
});
538539
```
539540

541+
You can also use a `sns.Topic` instead of an `sqs.Queue` as dead-letter queue:
542+
543+
```ts
544+
import * as sns from '@aws-cdk/aws-sns';
545+
546+
const dlt = new sns.Topic(this, 'DLQ');
547+
const fn = new lambda.Function(this, 'MyFunction', {
548+
runtime: lambda.Runtime.NODEJS_12_X,
549+
handler: 'index.handler',
550+
code: lambda.Code.fromInline('// your code here'),
551+
deadLetterTopic: dlt,
552+
});
553+
```
554+
540555
See [the AWS documentation](https://docs.aws.amazon.com/lambda/latest/dg/dlq.html)
541556
to learn more about AWS Lambdas and DLQs.
542557

packages/@aws-cdk/aws-lambda/lib/function.ts

+55-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as ec2 from '@aws-cdk/aws-ec2';
44
import * as iam from '@aws-cdk/aws-iam';
55
import * as kms from '@aws-cdk/aws-kms';
66
import * as logs from '@aws-cdk/aws-logs';
7+
import * as sns from '@aws-cdk/aws-sns';
78
import * as sqs from '@aws-cdk/aws-sqs';
89
import { Annotations, ArnFormat, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core';
910
import { Construct } from 'constructs';
@@ -188,11 +189,21 @@ export interface FunctionOptions extends EventInvokeConfigOptions {
188189

189190
/**
190191
* The SQS queue to use if DLQ is enabled.
192+
* If SNS topic is desired, specify `deadLetterTopic` property instead.
191193
*
192194
* @default - SQS queue with 14 day retention period if `deadLetterQueueEnabled` is `true`
193195
*/
194196
readonly deadLetterQueue?: sqs.IQueue;
195197

198+
/**
199+
* The SNS topic to use as a DLQ.
200+
* Note that if `deadLetterQueueEnabled` is set to `true`, an SQS queue will be created
201+
* rather than an SNS topic. Using an SNS topic as a DLQ requires this property to be set explicitly.
202+
*
203+
* @default - no SNS topic
204+
*/
205+
readonly deadLetterTopic?: sns.ITopic;
206+
196207
/**
197208
* Enable AWS X-Ray Tracing for Lambda Function.
198209
*
@@ -573,10 +584,15 @@ export class Function extends FunctionBase {
573584
public readonly grantPrincipal: iam.IPrincipal;
574585

575586
/**
576-
* The DLQ associated with this Lambda Function (this is an optional attribute).
587+
* The DLQ (as queue) associated with this Lambda Function (this is an optional attribute).
577588
*/
578589
public readonly deadLetterQueue?: sqs.IQueue;
579590

591+
/**
592+
* The DLQ (as topic) associated with this Lambda Function (this is an optional attribute).
593+
*/
594+
public readonly deadLetterTopic?: sns.ITopic;
595+
580596
/**
581597
* The architecture of this Lambda Function (this is an optional attribute and defaults to X86_64).
582598
*/
@@ -673,7 +689,15 @@ export class Function extends FunctionBase {
673689
this.addEnvironment(key, value);
674690
}
675691

676-
this.deadLetterQueue = this.buildDeadLetterQueue(props);
692+
// DLQ can be either sns.ITopic or sqs.IQueue
693+
const dlqTopicOrQueue = this.buildDeadLetterQueue(props);
694+
if (dlqTopicOrQueue !== undefined) {
695+
if (this.isQueue(dlqTopicOrQueue)) {
696+
this.deadLetterQueue = dlqTopicOrQueue;
697+
} else {
698+
this.deadLetterTopic = dlqTopicOrQueue;
699+
}
700+
}
677701

678702
let fileSystemConfigs: CfnFunction.FileSystemConfigProperty[] | undefined = undefined;
679703
if (props.filesystem) {
@@ -712,7 +736,7 @@ export class Function extends FunctionBase {
712736
environment: Lazy.uncachedAny({ produce: () => this.renderEnvironment() }),
713737
memorySize: props.memorySize,
714738
vpcConfig: this.configureVpc(props),
715-
deadLetterConfig: this.buildDeadLetterConfig(this.deadLetterQueue),
739+
deadLetterConfig: this.buildDeadLetterConfig(dlqTopicOrQueue),
716740
tracingConfig: this.buildTracingConfig(props),
717741
reservedConcurrentExecutions: props.reservedConcurrentExecutions,
718742
imageConfig: undefinedIfNoKeys({
@@ -1031,31 +1055,45 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett
10311055
};
10321056
}
10331057

1034-
private buildDeadLetterQueue(props: FunctionProps) {
1058+
private isQueue(deadLetterQueue: sqs.IQueue | sns.ITopic): deadLetterQueue is sqs.IQueue {
1059+
return (<sqs.IQueue>deadLetterQueue).queueArn !== undefined;
1060+
}
1061+
1062+
private buildDeadLetterQueue(props: FunctionProps): sqs.IQueue | sns.ITopic | undefined {
1063+
if (!props.deadLetterQueue && !props.deadLetterQueueEnabled && !props.deadLetterTopic) {
1064+
return undefined;
1065+
}
10351066
if (props.deadLetterQueue && props.deadLetterQueueEnabled === false) {
10361067
throw Error('deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false');
10371068
}
1038-
1039-
if (!props.deadLetterQueue && !props.deadLetterQueueEnabled) {
1040-
return undefined;
1069+
if (props.deadLetterTopic && (props.deadLetterQueue || props.deadLetterQueueEnabled !== undefined)) {
1070+
throw new Error('deadLetterQueue and deadLetterTopic cannot be specified together at the same time');
10411071
}
10421072

1043-
const deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', {
1044-
retentionPeriod: Duration.days(14),
1045-
});
1046-
1047-
this.addToRolePolicy(new iam.PolicyStatement({
1048-
actions: ['sqs:SendMessage'],
1049-
resources: [deadLetterQueue.queueArn],
1050-
}));
1073+
let deadLetterQueue: sqs.IQueue | sns.ITopic;
1074+
if (props.deadLetterTopic) {
1075+
deadLetterQueue = props.deadLetterTopic;
1076+
this.addToRolePolicy(new iam.PolicyStatement({
1077+
actions: ['sns:Publish'],
1078+
resources: [deadLetterQueue.topicArn],
1079+
}));
1080+
} else {
1081+
deadLetterQueue = props.deadLetterQueue || new sqs.Queue(this, 'DeadLetterQueue', {
1082+
retentionPeriod: Duration.days(14),
1083+
});
1084+
this.addToRolePolicy(new iam.PolicyStatement({
1085+
actions: ['sqs:SendMessage'],
1086+
resources: [deadLetterQueue.queueArn],
1087+
}));
1088+
}
10511089

10521090
return deadLetterQueue;
10531091
}
10541092

1055-
private buildDeadLetterConfig(deadLetterQueue?: sqs.IQueue) {
1093+
private buildDeadLetterConfig(deadLetterQueue?: sqs.IQueue | sns.ITopic) {
10561094
if (deadLetterQueue) {
10571095
return {
1058-
targetArn: deadLetterQueue.queueArn,
1096+
targetArn: this.isQueue(deadLetterQueue) ? deadLetterQueue.queueArn : deadLetterQueue.topicArn,
10591097
};
10601098
} else {
10611099
return undefined;

packages/@aws-cdk/aws-lambda/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@
110110
"@aws-cdk/aws-s3": "0.0.0",
111111
"@aws-cdk/aws-s3-assets": "0.0.0",
112112
"@aws-cdk/aws-signer": "0.0.0",
113+
"@aws-cdk/aws-sns": "0.0.0",
113114
"@aws-cdk/aws-sqs": "0.0.0",
114115
"@aws-cdk/core": "0.0.0",
115116
"@aws-cdk/cx-api": "0.0.0",
@@ -132,6 +133,7 @@
132133
"@aws-cdk/aws-s3": "0.0.0",
133134
"@aws-cdk/aws-s3-assets": "0.0.0",
134135
"@aws-cdk/aws-signer": "0.0.0",
136+
"@aws-cdk/aws-sns": "0.0.0",
135137
"@aws-cdk/aws-sqs": "0.0.0",
136138
"@aws-cdk/core": "0.0.0",
137139
"@aws-cdk/cx-api": "0.0.0",

packages/@aws-cdk/aws-lambda/test/function.test.ts

+118-8
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import * as kms from '@aws-cdk/aws-kms';
88
import * as logs from '@aws-cdk/aws-logs';
99
import * as s3 from '@aws-cdk/aws-s3';
1010
import * as signer from '@aws-cdk/aws-signer';
11+
import * as sns from '@aws-cdk/aws-sns';
1112
import * as sqs from '@aws-cdk/aws-sqs';
1213
import { testDeprecated } from '@aws-cdk/cdk-build-tools';
1314
import * as cdk from '@aws-cdk/core';
@@ -684,6 +685,84 @@ describe('function', () => {
684685
})).toThrow(/deadLetterQueue defined but deadLetterQueueEnabled explicitly set to false/);
685686
});
686687

688+
test('default function with SNS DLQ when client provides Topic to be used as DLQ', () => {
689+
const stack = new cdk.Stack();
690+
691+
const dlTopic = new sns.Topic(stack, 'DeadLetterTopic');
692+
693+
new lambda.Function(stack, 'MyLambda', {
694+
code: new lambda.InlineCode('foo'),
695+
handler: 'index.handler',
696+
runtime: lambda.Runtime.NODEJS_10_X,
697+
deadLetterTopic: dlTopic,
698+
});
699+
700+
const template = Template.fromStack(stack);
701+
template.hasResourceProperties('AWS::IAM::Policy', {
702+
PolicyDocument: {
703+
Statement: Match.arrayWith([
704+
{
705+
Action: 'sns:Publish',
706+
Effect: 'Allow',
707+
Resource: {
708+
Ref: 'DeadLetterTopicC237650B',
709+
},
710+
},
711+
]),
712+
},
713+
});
714+
template.hasResourceProperties('AWS::Lambda::Function', {
715+
DeadLetterConfig: {
716+
TargetArn: {
717+
Ref: 'DeadLetterTopicC237650B',
718+
},
719+
},
720+
});
721+
});
722+
723+
test('error when default function with SNS DLQ when client provides Topic to be used as DLQ and deadLetterQueueEnabled set to false', () => {
724+
const stack = new cdk.Stack();
725+
726+
const dlTopic = new sns.Topic(stack, 'DeadLetterTopic');
727+
728+
expect(() => new lambda.Function(stack, 'MyLambda', {
729+
code: new lambda.InlineCode('foo'),
730+
handler: 'index.handler',
731+
runtime: lambda.Runtime.NODEJS_10_X,
732+
deadLetterQueueEnabled: false,
733+
deadLetterTopic: dlTopic,
734+
})).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/);
735+
});
736+
737+
test('error when default function with SNS DLQ when client provides Topic to be used as DLQ and deadLetterQueueEnabled set to true', () => {
738+
const stack = new cdk.Stack();
739+
740+
const dlTopic = new sns.Topic(stack, 'DeadLetterTopic');
741+
742+
expect(() => new lambda.Function(stack, 'MyLambda', {
743+
code: new lambda.InlineCode('foo'),
744+
handler: 'index.handler',
745+
runtime: lambda.Runtime.NODEJS_10_X,
746+
deadLetterQueueEnabled: true,
747+
deadLetterTopic: dlTopic,
748+
})).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/);
749+
});
750+
751+
test('error when both topic and queue are presented as DLQ', () => {
752+
const stack = new cdk.Stack();
753+
754+
const dlQueue = new sqs.Queue(stack, 'DLQ');
755+
const dlTopic = new sns.Topic(stack, 'DeadLetterTopic');
756+
757+
expect(() => new lambda.Function(stack, 'MyLambda', {
758+
code: new lambda.InlineCode('foo'),
759+
handler: 'index.handler',
760+
runtime: lambda.Runtime.NODEJS_10_X,
761+
deadLetterQueue: dlQueue,
762+
deadLetterTopic: dlTopic,
763+
})).toThrow(/deadLetterQueue and deadLetterTopic cannot be specified together at the same time/);
764+
});
765+
687766
test('default function with Active tracing', () => {
688767
const stack = new cdk.Stack();
689768

@@ -1561,7 +1640,7 @@ describe('function', () => {
15611640
expect(logGroup.logGroupArn).toBeDefined();
15621641
});
15631642

1564-
test('dlq is returned when provided by user', () => {
1643+
test('dlq is returned when provided by user and is Queue', () => {
15651644
const stack = new cdk.Stack();
15661645

15671646
const dlQueue = new sqs.Queue(stack, 'DeadLetterQueue', {
@@ -1576,12 +1655,37 @@ describe('function', () => {
15761655
deadLetterQueue: dlQueue,
15771656
});
15781657
const deadLetterQueue = fn.deadLetterQueue;
1579-
expect(deadLetterQueue?.queueArn).toBeDefined();
1580-
expect(deadLetterQueue?.queueName).toBeDefined();
1581-
expect(deadLetterQueue?.queueUrl).toBeDefined();
1658+
const deadLetterTopic = fn.deadLetterTopic;
1659+
1660+
expect(deadLetterTopic).toBeUndefined();
1661+
1662+
expect(deadLetterQueue).toBeDefined();
1663+
expect(deadLetterQueue).toBeInstanceOf(sqs.Queue);
15821664
});
15831665

1584-
test('dlq is returned when setup by cdk', () => {
1666+
test('dlq is returned when provided by user and is Topic', () => {
1667+
const stack = new cdk.Stack();
1668+
1669+
const dlTopic = new sns.Topic(stack, 'DeadLetterQueue', {
1670+
topicName: 'MyLambda_DLQ',
1671+
});
1672+
1673+
const fn = new lambda.Function(stack, 'fn', {
1674+
handler: 'foo',
1675+
runtime: lambda.Runtime.NODEJS_10_X,
1676+
code: lambda.Code.fromInline('foo'),
1677+
deadLetterTopic: dlTopic,
1678+
});
1679+
const deadLetterQueue = fn.deadLetterQueue;
1680+
const deadLetterTopic = fn.deadLetterTopic;
1681+
1682+
expect(deadLetterQueue).toBeUndefined();
1683+
1684+
expect(deadLetterTopic).toBeDefined();
1685+
expect(deadLetterTopic).toBeInstanceOf(sns.Topic);
1686+
});
1687+
1688+
test('dlq is returned when setup by cdk and is Queue', () => {
15851689
const stack = new cdk.Stack();
15861690
const fn = new lambda.Function(stack, 'fn', {
15871691
handler: 'foo',
@@ -1590,9 +1694,12 @@ describe('function', () => {
15901694
deadLetterQueueEnabled: true,
15911695
});
15921696
const deadLetterQueue = fn.deadLetterQueue;
1593-
expect(deadLetterQueue?.queueArn).toBeDefined();
1594-
expect(deadLetterQueue?.queueName).toBeDefined();
1595-
expect(deadLetterQueue?.queueUrl).toBeDefined();
1697+
const deadLetterTopic = fn.deadLetterTopic;
1698+
1699+
expect(deadLetterTopic).toBeUndefined();
1700+
1701+
expect(deadLetterQueue).toBeDefined();
1702+
expect(deadLetterQueue).toBeInstanceOf(sqs.Queue);
15961703
});
15971704

15981705
test('dlq is undefined when not setup', () => {
@@ -1603,7 +1710,10 @@ describe('function', () => {
16031710
code: lambda.Code.fromInline('foo'),
16041711
});
16051712
const deadLetterQueue = fn.deadLetterQueue;
1713+
const deadLetterTopic = fn.deadLetterTopic;
1714+
16061715
expect(deadLetterQueue).toBeUndefined();
1716+
expect(deadLetterTopic).toBeUndefined();
16071717
});
16081718

16091719
test('one and only one child LogRetention construct will be created', () => {

0 commit comments

Comments
 (0)