Repository: serverless/serverless (JavaScript)
Date: 2020-01-21 11:51:17+00:00
Task type: Feature

Issue:
Support setting RedrivePolicty for AWS::SNS::Subscription when using sns as a trigger for a function
Similarly to the option of using a FilterPolicy when defining a sns topic as the trigger of a function, it would be useful to allow to define the dead letter target (the name or the arn) so the generated code looks something like 
````
{
                  Type: 'AWS::SNS::Subscription',
                  Properties: {
                    TopicArn: topicArn,
                    Protocol: 'lambda',
                    Endpoint: endpoint,
                    FilterPolicy: event.sns.filterPolicy,
                    RedrivePolicy: {
                        deadLetterTargetArn: {"Fn::GetAtt": [event.sns.dlqName, "Arn"]}
                    }
                    Region: region,
                  },
 },
````



Issue discussion:
Thanks @tcastelli for suggestion. We're definitely open for PR that provides that

Solution patch:
diff --git a/docs/providers/aws/events/sns.md b/docs/providers/aws/events/sns.md
index 4c9ecd86da1..a973a510a7e 100644
--- a/docs/providers/aws/events/sns.md
+++ b/docs/providers/aws/events/sns.md
@@ -146,3 +146,25 @@ functions:
               - dog
               - cat
 ```
+
+## Setting a redrive policy
+
+This event definition creates an SNS topic that sends messages to a Dead Letter Queue (defined by its ARN) when the associated lambda is not available. In this example, messages that aren't delivered to the `dispatcher` Lambda (because the lambda service is down or irresponsive) will end in `myDLQ`
+
+```yml
+functions:
+  dispatcher:
+    handler: dispatcher.handler
+    events:
+      - sns:
+          topicName: dispatcher
+          redrivePolicy:
+            deadLetterTargetArn: !Ref myDLQ
+
+resources:
+  Resources:
+    myDLQ:
+      Type: AWS::SQS::Queue
+      Properties:
+        QueueName: myDLQ
+```
diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md
index 7eedcab9c29..322fdd3e215 100644
--- a/docs/providers/aws/guide/serverless.yml.md
+++ b/docs/providers/aws/guide/serverless.yml.md
@@ -252,6 +252,12 @@ functions:
       - sns:
           topicName: aggregate
           displayName: Data aggregation pipeline
+          filterPolicy:
+            pet:
+              - dog
+              - cat
+          redrivePolicy:
+            deadLetterTargetArn: arn:aws:sqs:region:XXXXXX:myDLQ
       - sqs:
           arn: arn:aws:sqs:region:XXXXXX:myQueue
           batchSize: 10
diff --git a/lib/plugins/aws/package/compile/events/sns/index.js b/lib/plugins/aws/package/compile/events/sns/index.js
index 1ede2b364a2..3b62dc379b2 100644
--- a/lib/plugins/aws/package/compile/events/sns/index.js
+++ b/lib/plugins/aws/package/compile/events/sns/index.js
@@ -142,6 +142,7 @@ class AwsCompileSNSEvents {
                     Protocol: 'lambda',
                     Endpoint: endpoint,
                     FilterPolicy: event.sns.filterPolicy,
+                    RedrivePolicy: event.sns.redrivePolicy,
                     Region: region,
                   },
                 },
@@ -182,7 +183,10 @@ class AwsCompileSNSEvents {
                 });
               }
 
-              if (event.sns.filterPolicy) {
+              if (
+                event.sns.filterPolicy ||
+                (event.sns.redrivePolicy && event.sns.redrivePolicy.deadLetterTargetArn)
+              ) {
                 _.merge(template.Resources, {
                   [subscriptionLogicalId]: {
                     Type: 'AWS::SNS::Subscription',
@@ -191,6 +195,7 @@ class AwsCompileSNSEvents {
                         Ref: topicLogicalId,
                       },
                       FilterPolicy: event.sns.filterPolicy,
+                      RedrivePolicy: event.sns.redrivePolicy,
                     }),
                   },
                 });
@@ -218,6 +223,40 @@ class AwsCompileSNSEvents {
                 },
               },
             });
+
+            if (event.sns.redrivePolicy && event.sns.redrivePolicy.deadLetterTargetArn) {
+              const queuePolicyLogicalId = this.provider.naming.getQueueLogicalId(
+                functionName,
+                `${topicName}DLQ`
+              );
+              Object.assign(template.Resources, {
+                [queuePolicyLogicalId]: {
+                  Type: 'AWS::SQS::QueuePolicy',
+                  Properties: {
+                    PolicyDocument: {
+                      Version: '2012-10-17',
+                      Id: queuePolicyLogicalId,
+                      Statement: [
+                        {
+                          Effect: 'Allow',
+                          Principal: {
+                            Service: 'sns.amazonaws.com',
+                          },
+                          Action: 'sqs:SendMessage',
+                          Resource: event.sns.redrivePolicy.deadLetterTargetArn,
+                          Condition: {
+                            ArnEquals: {
+                              'aws:SourceArn': topicArn,
+                            },
+                          },
+                        },
+                      ],
+                    },
+                    Queues: [event.sns.redrivePolicy.deadLetterTargetArn],
+                  },
+                },
+              });
+            }
           }
         });
       }


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/sns/index.test.js b/lib/plugins/aws/package/compile/events/sns/index.test.js
index 1eeab35c410..b2ddabc7fc7 100644
--- a/lib/plugins/aws/package/compile/events/sns/index.test.js
+++ b/lib/plugins/aws/package/compile/events/sns/index.test.js
@@ -596,5 +596,60 @@ describe('AwsCompileSNSEvents', () => {
           .FirstLambdaPermissionBarSNS.Type
       ).to.equal('AWS::Lambda::Permission');
     });
+
+    it('should link topic to corresponding dlq when redrivePolicy is defined', () => {
+      awsCompileSNSEvents.serverless.service.functions = {
+        first: {
+          events: [
+            {
+              sns: {
+                topicName: 'Topic 1',
+                displayName: 'Display name for topic 1',
+                redrivePolicy: {
+                  deadLetterTargetArn: {
+                    'Fn::GetAtt': ['SNSDLQ', 'Arn'],
+                  },
+                },
+              },
+            },
+          ],
+        },
+      };
+
+      Object.assign(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources,
+        {
+          SNSDLQ: {
+            Type: 'AWS::SQS::Queue',
+            Properties: {
+              QueueName: 'SNSDLQ',
+            },
+          },
+        }
+      );
+
+      awsCompileSNSEvents.compileSNSEvents();
+
+      expect(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .SNSTopicTopic1.Type
+      ).to.equal('AWS::SNS::Topic');
+      expect(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .FirstLambdaPermissionTopic1SNS.Type
+      ).to.equal('AWS::Lambda::Permission');
+      expect(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .FirstSnsSubscriptionTopic1.Type
+      ).to.equal('AWS::SNS::Subscription');
+      expect(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .FirstSnsSubscriptionTopic1.Properties.RedrivePolicy
+      ).to.eql({ deadLetterTargetArn: { 'Fn::GetAtt': ['SNSDLQ', 'Arn'] } });
+      expect(
+        awsCompileSNSEvents.serverless.service.provider.compiledCloudFormationTemplate.Resources
+          .FirstEventSourceMappingSQSTopic1DLQ.Type
+      ).to.equal('AWS::SQS::QueuePolicy');
+    });
   });
 });