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

Issue:
AWS Stack Rollback Configuration 
# Feature Proposal
Set Rollback Configuration using AWS provider configuration.

## Description
I would like our serverless framework deployments to be safer. CloudFormation can automatically [monitor and roll back stack operations]. I just need the serverless framework to set the `RollbackConfiguration` in calls to createStack, [updateStack] or createChangeSet.

I think that it is natural to specify the configuration with a new `rollbackConfiguration` property in `serverless.yml`. For example:

```yaml
provider:
  name: aws
  rollbackConfiguration:
    MonitoringTimeInMinutes: 20
    RollbackTriggers:
      - Arn: arn:aws:cloudwatch:us-east-1:000000000000:alarm:health
        Type: AWS::CloudWatch::Alarm
      - Arn: arn:aws:cloudwatch:us-east-1:000000000000:alarm:latency
        Type: AWS::CloudWatch::Alarm
```

## Similar or dependent issues
[serverless-plugin-canary-deployments] serves a similar role. However, it only works with certain triggers (I need CloudFront triggers, which are not supported). The plugin only affects rollback of the lambda function version, which leaves us vulnerable to configuration problems in the CloudFormation template.

[monitor and roll back stack operations]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-rollback-triggers.html
[updateStack]: https://github.com/serverless/serverless/blob/f93b27bf684d9a14b1e67ec554a7719ca3628135/lib/plugins/aws/lib/updateStack.js#L54
[serverless-plugin-canary-deployments]: https://github.com/davidgf/serverless-plugin-canary-deployments


Issue discussion:

Solution patch:
diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md
index 799a10cacfb..7eedcab9c29 100644
--- a/docs/providers/aws/guide/serverless.yml.md
+++ b/docs/providers/aws/guide/serverless.yml.md
@@ -129,6 +129,13 @@ provider:
         IpAddress:
           aws:SourceIp:
             - '123.123.123.123'
+    rollbackConfiguration:
+      MonitoringTimeInMinutes: 20
+      RollbackTriggers:
+        - Arn: arn:aws:cloudwatch:us-east-1:000000000000:alarm:health
+          Type: AWS::CloudWatch::Alarm
+        - Arn: arn:aws:cloudwatch:us-east-1:000000000000:alarm:latency
+          Type: AWS::CloudWatch::Alarm
   tags: # Optional service wide function tags
     foo: bar
     baz: qux
diff --git a/lib/plugins/aws/lib/updateStack.js b/lib/plugins/aws/lib/updateStack.js
index 0677d2d6336..bf8d67bd96d 100644
--- a/lib/plugins/aws/lib/updateStack.js
+++ b/lib/plugins/aws/lib/updateStack.js
@@ -98,6 +98,10 @@ module.exports = {
       });
     }
 
+    if (this.serverless.service.provider.rollbackConfiguration) {
+      params.RollbackConfiguration = this.serverless.service.provider.rollbackConfiguration;
+    }
+
     return this.provider
       .request('CloudFormation', 'updateStack', params)
       .then(cfData => this.monitorStack('update', cfData))


Test patch:
diff --git a/lib/plugins/aws/lib/updateStack.test.js b/lib/plugins/aws/lib/updateStack.test.js
index 6398d15ee33..076f86a6144 100644
--- a/lib/plugins/aws/lib/updateStack.test.js
+++ b/lib/plugins/aws/lib/updateStack.test.js
@@ -187,6 +187,29 @@ describe('updateStack', () => {
         expect(updateStackStub.args[0][2].NotificationARNs).to.deep.equal([mytopicArn]);
       });
     });
+
+    it('should use use rollbackConfiguration if it is specified', () => {
+      const myRollbackConfiguration = {
+        MonitoringTimeInMinutes: 20,
+        RollbackTriggers: [
+          {
+            Arn: 'arn:aws:cloudwatch:us-east-1:000000000000:alarm:health',
+            Type: 'AWS::CloudWatch::Alarm',
+          },
+          {
+            Arn: 'arn:aws:cloudwatch:us-east-1:000000000000:alarm:latency',
+            Type: 'AWS::CloudWatch::Alarm',
+          },
+        ],
+      };
+      awsDeploy.serverless.service.provider.rollbackConfiguration = myRollbackConfiguration;
+
+      return awsDeploy.update().then(() => {
+        expect(updateStackStub.args[0][2].RollbackConfiguration).to.deep.equal(
+          myRollbackConfiguration
+        );
+      });
+    });
   });
 
   describe('#updateStack()', () => {