Repository: serverless/serverless (JavaScript)
Date: 2019-03-15 13:54:09+00:00
Task type: Bug Fix

Issue:
Not possible to create resources that depend on the ApiGateway::Deployment
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->
# This is a Bug Report
## Description

For bug reports:
- What went wrong?

I am creating a custom resource `AWS::ApiGateway::BasePathMapping` like this:

```
pathmapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties:
        BasePath: oauth2
        DomainName: ${self:custom.domainName}
        RestApiId: 
          Ref: ApiGatewayRestApi
        Stage: ${self:custom.stage}
```

But the Cloudformation stack fails. It fails because the BasePathMapping requires the API to be deployed but it does not depend on the `AWS::ApiGateway::Deployment`
- What did you expect should have happened?

The cloudformation stack should not fail. The BasePathMapping should be able to depend on the Deployment.
- What was the config you used?

Serverless RC2
- What stacktrace or error message from your provider did you see?

> Invalid stage identifier specified
## Additional Data
- **_Serverless Framework Version you're using**_: RC2
- **_Operating System**_: Mac OS X
- **_Stack Trace**_: None. The cloudformation stack is fine but fails to update.
- **_Provider Error messages**_:

This is the error from CloudFormation when creating the BasePathMapping

> Invalid stage identifier specified
## Workaround

It is not possible to DependsOn the deployment because the logical name of the deployment contains a random number (based on the time in ms).

I just tried to copy the entire cloudformation stack json and change the logical name of the deployment to ApiGatewayDeployment and then make my BasePathMapping DependsOn that and now it works.

This is not a good workaround because it defeats the purpose of sls.

The way I got around it to depend on an ApiKey since they are named with a sequental number.
So I create an ApiKey in the provider section:

```
provider:
    apiKeys:
        - workaroundkey
```

And then I can make my BasePathMapping depend on this:

```
pathmapping:
    Type: AWS::ApiGateway::BasePathMapping
    DependsOn: ApiGatewayApiKey1
    Properties:
        BasePath: oauth2
        DomainName: ${self:custom.domainName}
        RestApiId: 
          Ref: ApiGatewayRestApi
        Stage: ${self:custom.stage}
```
### Regression

The unique number was introduced in https://github.com/serverless/serverless/pull/1761

Before that it was possible to DependsOn the logical name of the Deployment.



Issue discussion:
thanks @alexanderbh for reporting. The main problem is we need to append a random number to the deployment, otherwise on the next deployment nothing happens and the API updates don't get deployed (you can test that locally when removing the random part).

The only real way I see at the moment to do this would be using a Cloudformation Mapping and storing the random number in that Cloudformation Mapping: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/mappings-section-structure.html

This way you could depend on it because you get the value for the ref out of the mapping.

Yeah that is what I got from the earlier talks relating to the random number.

I am gonna try to add the mapping to the stack and see how that works. If I get it to work I will make a pull request.

It is not possible to use a function in DependsOn. DependsOn must be a string or a list of strings. I had made the mapping solution but it does not work when I try to use the Fn::FindInMap as DependsOn.

I am going to try to fix the force-redeploy instead. It would be more clean to remove the random number than working around it with maps.

Another workaround would be generating this 'random slug' at command invoke time, so it's available in serverless.yml for expansion. 

DependsOn: 'ApiGatewayDeployment${slug}'

I just published a serverless plugin that essentially allows what @dougmoscrop describes - it lets you bind the dynamic deployment id to your custom resources at runtime. You can find the plugin here: https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id
@flomotlik @alexanderbh 
Because the random number is an internal workaround, potentially a just a temporary one, I doesn't make sense to expose it for us to use in serverless.yml as @dougmoscrop suggests. What I suggest instead is to scan through references of `ApiGatewayDeployment`, e.g. `DependsOn: ApiGatewayDeployment` inside the compiled CF template resources and append the random number. This way it remains transparent to users and doesn't introduce breaking changes if the random number workaround is eliminated, in light of a better solution.

@simoami 
Indeed, that's what the plugin does, and if the workaround is removed, would continue to work, because it would pick up the new naming convention of the `ApiGatewayDeployment`. I did choose to look for a sentinel value (by default `__deployment__`) and replace it entirely with the deployment id, rather than only appending the random number onto `ApiGatewayDeployment`, since the name `ApiGatewayDeployment` could also potentially change in the future. Using a non-standard sentinel value also makes it clear that there is some magic going on. Of course, I added a hook to change the sentinel value, so you could very easily set it to `ApiGatewayDeployment` and everything would work as you describe. You can change the sentinel value with:

```yaml
custom:
  deploymentId:
    variableSyntax: ApiGatewayDeployment
 ```
@jacob-meacham Thanks for the plugin. I will be helpful in the short term. 

I wouldn't worry about hardcoding `ApiGatewayDeployment` because the plugin exists to address a very specific issue with serverless that affects a single template name (sorry if this is inaccurate. I didn't review the plugin yet). Because of the suggested sentinel value, the yaml file now has to include proprietary values to the resource definitions. Can it be optional as in only specify the plugin name is enough by default?
Are you suggesting that I should change the sentinel value from `__deployment__` to `ApiGatewayDeployment`? I'd be open to doing so. 

You mentioned the pros of that approach: 
* the customResources in serverless.yml look like standard CloudFormation templates
* if/when this is no longer required in serverless, users can just remove the plugin and everything will continue to work. 

I think the cons are that it hides what's going on in a way I fear could bite people in a way similar to this issue. Using a clearly invalid key means that no one is fooled into thinking that `ApiGatewayDeployment` works on its own.
I like very much the idea of having an explicitly 'magical' sentinel value as a default. 
Jacob was kind enough to support changing it through configuration, so I don't see any problems with the current settings. Just my 2c
@jacob-meacham Now that I got some time to think about this, I have a suggestion for you. Your plugin could be made very useful if it supported a bunch of more magic values that are internal to serverless. including new ones that you generate, like uuid, random time number...etc. And so one can use: `${self:custom.magic.deploymentId}`, `${self:custom.magic.sequentialId` or `${self:custom.magic.uuid}`. In my opinion this would make the plugin extremely useful even outside of the current use case.
So I'm having a hell of a time with this issue. Has there been any progress on this front?
Same here. +1 for @simoami's suggestion
Same problem here with [`AWS::Serverless::Api`](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi) ([SAM](https://github.com/awslabs/serverless-application-model))

```yml
  ApiGatewayApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: Foo
  BasePathMapping:
    Type: "AWS::ApiGateway::BasePathMapping"
    Properties:
      Stage: Foo
```

> **UPDATE_FAILED**	AWS::ApiGateway::BasePathMapping	BasePathMapping	Invalid stage identifier specified
@yvele Do you have a AWS::ApiGateway::Stage resource in your stack that is named "Foo" ?
I'm quite sure that the BasePathMapping.Stage property can only reference a Stage resource, not a name. This is imo only possible with the Deployment resource.
@HyperBrain Isn't [`AWS::Serverless::Api`](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi) supposed to **create** the `Foo` stage?

Edit: [Yes it is](https://github.com/serverless/serverless/issues/2233#issuecomment-297995406)
As far as my experiments with that were, it does not (maybe implicitly within APIG, but not explicitly as AWS::ApiGateway::Stage resource in your stack).
@HyperBrain I just run the test and I can confirm that [`AWS::Serverless::Api`](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessapi) is successfully creating the the `Foo` stage.. But I'm not able to depend on it (neither implicitly nor explicitly via `depensOn`) .. 😭 
Unfortunately, the handy plugin at https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id is broken as of Serverless 1.12.
@ajkerr Can you copy the actual error message here? It needs to be checked if the plugin tries to use or modify some internal state, or one of its hooks is wrong.
Additionally please copy the used serverless command line (the old compatible `serverless deploy` should work).
@HyperBrain Sure thing. Here's the error:
``` 
Serverless Error ---------------------------------------

Template format error: Unresolved resource dependencies
[__deployment__] in the Resources block of the template 
```

This error is actually coming from AWS, because the plugin didn't do what it intended to do, which was to replace `__deployment__` with the actual generated deployment id.

The issue appears to be that the code is modifying internal state here: https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id/blob/master/src/index.js#L28

This no longer seems to have any affect with the changes to the lifecycle hooks made in 1.12.

Perhaps there's another hook that can be plugged into earlier in the lifecycle, where modifying this value makes sense?
I think the error is that the plugin operates on  `this.serverless.service.resources.Resources` instead of `this.serverless.service.provider.compiledCloudFormationTemplate`. Plugins should not modify the immutable service definition, but the mutable compiled template. I assume that at the time the plugin tries to modify the service definition, the compiled template already has been generated and serverless does continue with the compiled template for deployment.
See https://gist.github.com/HyperBrain/bba5c9698e92ac693bb461c99d6cfeec for the new events. I wonder that the plugin did not use one of the old deploy:XXX events to do the modifications, but just before:deploy:deploy.
This behavior will especially break when applying the new package/deploy commands with `--package`.

BTW: Which command line did you use exactly?
@HyperBrain @ajkerr thanks for the feedback/suggestions - I'll take a look at this today and see if I can't get it working again.
@jacob-meacham I just updated the PR I submitted yesterday, and I think it's working properly now.
Any progress on this issue? My current workaround is commenting out the `AWS::ApiGateway::BasePathMapping` for the first deploy run and then commenting it in again. 
@schickling have you looked into https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id which tackles this issue?

Unfortunately we had to do this "hack" so that API Gateway picks up the changes. Not sure if this quirk was fixed by AWS in the meantime 🤔 
@pmuens I too am experiencing this issue for the "first" deployment, after that it's fine. But it's more than just a bit hacky and potentially very confusing to someone taking the project and trying to create their own deployment.
I set my basepathmappings dependent on CreateLambdaPermissionApiGateway, which seems to have worked, FWIW.
Is there any way to verify if this hack is still required or if it can be reversed now? Another side effect is that the Api Gateway Deployment is always updated every time the cloudformation stack is updated even if there are no api related changes
@talawahtech thank for commenting 👍

Unfortunately this hack is still required. Have you tried the https://github.com/jacob-meacham/serverless-plugin-bind-deployment-id plugin to work around this?
@pmuens, thanks for you suggestion. I have been working around it by setting the DependsOn attribute to {FunctionName}LambdaPermissionApiGateway. I prefer this approach because I don't have to rely on a plug-in being kept up to date.
> @pmuens, thanks for you suggestion. I have been working around it by setting the DependsOn attribute to {FunctionName}LambdaPermissionApiGateway. I prefer this approach because I don't have to rely on a plug-in being kept up to date.

Thanks for commenting a posting your solution @talawahtech 👍

Sounds like a good approach for now!
@talawahtech @pmuens  That did not work for me. I also don't like the idea of using a plugin that has not been updated in 6 months.
@stevenmwade are you replacing {FunctionName} with normalized name of your function? You can confirm the actual name by searching for "LambdaPermissionApiGateway" in the auto-generated cloudformation template (cloudformation-template-update-stack.json) that serverless creates in the .serverless folder when you do a deploy.
@talawahtech I did. Instead of failing at 25 seconds, it fails at 18 minutes and gives the same error. I'm back to two deploys.
Just in case someone find some issues like me: I was not able to fix using @talawahtech solution but found something similar.
I finally added a DependsOn: [LambdaAPIDefinitionStage] in my 'AWS::ApiGateway::BasePathMapping' element. In my case, my 'AWS::Serverless::Api' element was called LambdaAPIDefinition.
Also, I was not able to locate the cloudformation-template-update-stack.json, but was able to find something that I understand is similar by going to AWS console > Cloudformation > (your stack) > other actions > view / edit template in Designer .
Thanks a lot @talawahtech, your comment allowed me to find a solution :)
FWIW, another solution to this problem is to use the [additional stacks plugin](https://github.com/SC5/serverless-plugin-additional-stacks) and define your base path mappings (and route53 entries if necessary) as an additional stack, like this:

```
custom:
  additionalStacks:
    pathMappings:
      Deploy: After
      Resources:
          myDomainName:
              Type: AWS::ApiGateway::DomainName
              Properties:
                  DomainName: my-domain.com
          myDomainMapping:
              DependsOn: myDomainName
              Type: AWS::ApiGateway::BasePathMapping
              Properties:
                  DomainName: "Fn::GetAtt": "myDomainName.DomainName"
                  RestApiId:
                       "Fn::ImportValue": "api-gw-id-${self:provider.stage}"
                  Stage: ${self:provider.stage}
```
The `Deploy: After` line ensures that when calling `sls deploy`, this additional stack is deployed last, which means the API Gateway exists and can be referenced by the additional stack. Note that in my example, I've referenced an output from the main sls stack:
```
Outputs:
    APIGatewayID:
      Value:
        "Ref": ApiGatewayRestApi
      Export:
        Name: api-gw-id-${self:provider.stage}
```
Is the random number problem still an AWS issue?  Has anyone tried reverting that change to see if its easier to depend on the deployment?
I was able to make this work by adding:

```
DependsOn:
  - <LambdaNameGoesHere>LambdaPermissionApiGateway
```

to my `AWS::ApiGateway::BasePathMapping` resource creation
The `xxxPermissionApiGateway` trick doesn't work reliably for me. The only way I was able to get this working reliably was by using the plugin.

Solution patch:
diff --git a/docs/providers/aws/guide/resources.md b/docs/providers/aws/guide/resources.md
index 4384818fd8b..46e085729b6 100644
--- a/docs/providers/aws/guide/resources.md
+++ b/docs/providers/aws/guide/resources.md
@@ -52,12 +52,12 @@ You can overwrite/attach any kind of resource to your CloudFormation stack. You
 
 To have consistent naming in the CloudFormation Templates that get deployed we use a standard pattern:
 
-`{Function Name}{Cloud Formation Resource Type}{Resource Name}{SequentialID or Random String}`
+`{Function Name}{Cloud Formation Resource Type}{Resource Name}{SequentialID, instanceId or Random String}`
 
 * `Function Name` - This is optional for Resources that should be recreated when the function name gets changed. Those resources are also called *function bound*
 * `Cloud Formation Resource Type` - E.g., S3Bucket
 * `Resource Name` - An identifier for the specific resource, e.g. for an S3 Bucket the configured bucket name.
-* `SequentialID or Random String` - For a few resources we need to add an optional sequential id or random string to identify them
+* `SequentialID, instanceId or Random String` - For a few resources we need to add an optional sequential id, the Serverless instanceId (accessible via `${sls:instanceId}`) or a random string to identify them
 
 All resource names that are deployed by Serverless have to follow this naming scheme. The only exception (for backwards compatibility reasons) is the S3 Bucket that is used to upload artifacts so they can be deployed to your function.
 
@@ -81,7 +81,7 @@ If you are unsure how a resource is named, that you want to reference from your
 |ApiGateway::Resource   | ApiGatewayResource{normalizedPath}                      | ApiGatewayResourceUsers       |
 |ApiGateway::Method     | ApiGatewayMethod{normalizedPath}{normalizedMethod}      | ApiGatewayMethodUsersGet      |
 |ApiGateway::Authorizer | {normalizedFunctionName}ApiGatewayAuthorizer            | HelloApiGatewayAuthorizer     |
-|ApiGateway::Deployment | ApiGatewayDeployment{randomNumber}                      | ApiGatewayDeployment12356789  |
+|ApiGateway::Deployment | ApiGatewayDeployment{instanceId}                      | ApiGatewayDeployment12356789  |
 |ApiGateway::ApiKey     | ApiGatewayApiKey{SequentialID}                          | ApiGatewayApiKey1             |
 |ApiGateway::UsagePlan  | ApiGatewayUsagePlan                          | ApiGatewayUsagePlan             |
 |ApiGateway::UsagePlanKey     | ApiGatewayUsagePlanKey{SequentialID}                          | ApiGatewayUsagePlanKey1             |
diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md
index 71a846240b9..804820b2243 100644
--- a/docs/providers/aws/guide/variables.md
+++ b/docs/providers/aws/guide/variables.md
@@ -33,6 +33,7 @@ You can define your own variable syntax (regex) if it conflicts with CloudFormat
 
 ## Current variable sources:
 
+- [Serverless Core variables](#referencing-serverless-core-variables)
 - [Environment variables](#referencing-environment-variables)
 - [CLI options](#referencing-cli-options)
 - [Other properties defined in `serverless.yml`](#reference-properties-in-serverlessyml)
@@ -103,6 +104,27 @@ resources:
 
 In the above example you're setting a global schedule for all functions by referencing the `globalSchedule` property in the same `serverless.yml` file. This way, you can easily change the schedule for all functions whenever you like.
 
+## Referencing Serverless Core Variables
+Serverless initializes core variables which are used internally by the Framework itself. Those values are exposed via the Serverless Variables system and can be re-used with the `{sls:}` variable prefix.
+
+The following variables are available:
+
+**instanceId**
+
+A random id which will be generated whenever the Serverless CLI is run. This value can be used when predictable random variables are required.
+
+```yml
+service: new-service
+provider: aws
+
+functions:
+  func1:
+    name: function-1
+    handler: handler.func1
+    environment:
+      APIG_DEPLOYMENT_ID: ApiGatewayDeployment${sls:instanceId}
+```
+
 ## Referencing Environment Variables
 To reference environment variables, use the `${env:SOME_VAR}` syntax in your `serverless.yml` configuration file.  It is valid to use the empty string in place of `SOME_VAR`.  This looks like "`${env:}`" and the result of declaring this in your `serverless.yml` is to embed the complete `process.env` object (i.e. all the variables defined in your environment).
 
@@ -296,7 +318,7 @@ functions:
     name: hello
     handler: handler.hello
 custom:
-  supersecret: 
+  supersecret:
     num: 1
     str: secret
     arr:
diff --git a/lib/Serverless.js b/lib/Serverless.js
index 8332e96e5d6..be891e355a9 100644
--- a/lib/Serverless.js
+++ b/lib/Serverless.js
@@ -48,6 +48,9 @@ class Serverless {
   }
 
   init() {
+    // create an instanceId (can be e.g. used when a predictable random value is needed)
+    this.instanceId = (new Date()).getTime().toString();
+
     // create a new CLI instance
     this.cli = new this.classes.CLI(this);
 
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 10036b51d20..1d35bb71798 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -47,6 +47,7 @@ class Variables {
     this.deepRefSyntax = RegExp(/(\${)?deep:\d+(\.[^}]+)*()}?/);
     this.overwriteSyntax = RegExp(/\s*(?:,\s*)+/g);
     this.fileRefSyntax = RegExp(/^file\(([^?%*:|"<>]+?)\)/g);
+    this.slsRefSyntax = RegExp(/^sls:/g);
     this.envRefSyntax = RegExp(/^env:/g);
     this.optRefSyntax = RegExp(/^opt:/g);
     this.selfRefSyntax = RegExp(/^self:/g);
@@ -516,7 +517,9 @@ class Variables {
     if (this.tracker.contains(variableString)) {
       ret = this.tracker.get(variableString, propertyString);
     } else {
-      if (variableString.match(this.envRefSyntax)) {
+      if (variableString.match(this.slsRefSyntax)) {
+        ret = this.getValueFromSls(variableString);
+      } else if (variableString.match(this.envRefSyntax)) {
         ret = this.getValueFromEnv(variableString);
       } else if (variableString.match(this.optRefSyntax)) {
         ret = this.getValueFromOptions(variableString);
@@ -547,6 +550,15 @@ class Variables {
     return ret;
   }
 
+  getValueFromSls(variableString) {
+    let valueToPopulate = {};
+    const requestedSlsVar = variableString.split(':')[1];
+    if (requestedSlsVar === 'instanceId') {
+      valueToPopulate = this.serverless.instanceId;
+    }
+    return BbPromise.resolve(valueToPopulate);
+  }
+
   getValueFromEnv(variableString) { // eslint-disable-line class-methods-use-this
     const requestedEnvVar = variableString.split(':')[1];
     let valueToPopulate;
diff --git a/lib/plugins/aws/lib/naming.js b/lib/plugins/aws/lib/naming.js
index 700d9fab68f..ad314c8899e 100644
--- a/lib/plugins/aws/lib/naming.js
+++ b/lib/plugins/aws/lib/naming.js
@@ -193,8 +193,8 @@ module.exports = {
     return `${this.getNormalizedWebsocketsRouteKey(route)}WebsocketsRoute`;
   },
 
-  getWebsocketsDeploymentLogicalId() {
-    return `WebsocketsDeployment${(new Date()).getTime().toString()}`;
+  getWebsocketsDeploymentLogicalId(id) {
+    return `WebsocketsDeployment${id}`;
   },
 
   getWebsocketsStageLogicalId() {
@@ -213,8 +213,8 @@ module.exports = {
     }
     return `${this.provider.getStage()}-${this.provider.serverless.service.service}`;
   },
-  generateApiGatewayDeploymentLogicalId() {
-    return `ApiGatewayDeployment${(new Date()).getTime().toString()}`;
+  generateApiGatewayDeploymentLogicalId(id) {
+    return `ApiGatewayDeployment${id}`;
   },
   getRestApiLogicalId() {
     return 'ApiGatewayRestApi';
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
index 57e16cc053b..10056198495 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/deployment.js
@@ -6,7 +6,7 @@ const BbPromise = require('bluebird');
 module.exports = {
   compileDeployment() {
     this.apiGatewayDeploymentLogicalId = this.provider.naming
-      .generateApiGatewayDeploymentLogicalId();
+      .generateApiGatewayDeploymentLogicalId(this.serverless.instanceId);
 
     _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
       [this.apiGatewayDeploymentLogicalId]: {
diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js
index a26a341bdf6..ace0f18336f 100644
--- a/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js
+++ b/lib/plugins/aws/package/compile/events/websockets/lib/deployment.js
@@ -11,7 +11,7 @@ module.exports = {
       return routeLogicalId;
     });
     this.websocketsDeploymentLogicalId = this.provider.naming
-      .getWebsocketsDeploymentLogicalId();
+      .getWebsocketsDeploymentLogicalId(this.serverless.instanceId);
 
     _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
       [this.websocketsDeploymentLogicalId]: {


Test patch:
diff --git a/lib/Serverless.test.js b/lib/Serverless.test.js
index cb31dedc68e..abdc4dc5a3e 100644
--- a/lib/Serverless.test.js
+++ b/lib/Serverless.test.js
@@ -125,6 +125,10 @@ describe('Serverless', () => {
       serverless.pluginManager.updateAutocompleteCacheFile.restore();
     });
 
+    it('should set an instanceId', () => serverless.init().then(() => {
+      expect(serverless.instanceId).to.match(/\d/);
+    }));
+
     it('should create a new CLI instance', () => serverless.init().then(() => {
       expect(serverless.cli).to.be.instanceof(CLI);
     }));
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index e0df9382ef1..bac5203e36f 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -1328,97 +1328,119 @@ module.exports = {
   });
 
   describe('#getValueFromSource()', () => {
-    it('should call getValueFromEnv if referencing env var', () => {
-      const getValueFromEnvStub = sinon.stub(serverless.variables, 'getValueFromEnv')
+    const variableValue = 'variableValue';
+    let getValueFromSlsStub;
+    let getValueFromEnvStub;
+    let getValueFromOptionsStub;
+    let getValueFromSelfStub;
+    let getValueFromFileStub;
+    let getValueFromCfStub;
+    let getValueFromS3Stub;
+    let getValueFromSsmStub;
+
+    beforeEach(() => {
+      getValueFromSlsStub = sinon.stub(serverless.variables, 'getValueFromSls')
+        .resolves('variableValue');
+      getValueFromEnvStub = sinon.stub(serverless.variables, 'getValueFromEnv')
+        .resolves('variableValue');
+      getValueFromOptionsStub = sinon.stub(serverless.variables, 'getValueFromOptions')
+        .resolves('variableValue');
+      getValueFromSelfStub = sinon.stub(serverless.variables, 'getValueFromSelf')
+        .resolves('variableValue');
+      getValueFromFileStub = sinon.stub(serverless.variables, 'getValueFromFile')
         .resolves('variableValue');
-      return serverless.variables.getValueFromSource('env:TEST_VAR').should.be.fulfilled
+      getValueFromCfStub = sinon.stub(serverless.variables, 'getValueFromCf')
+        .resolves('variableValue');
+      getValueFromS3Stub = sinon.stub(serverless.variables, 'getValueFromS3')
+        .resolves('variableValue');
+      getValueFromSsmStub = sinon.stub(serverless.variables, 'getValueFromSsm')
+        .resolves('variableValue');
+    });
+
+    afterEach(() => {
+      serverless.variables.getValueFromSls.restore();
+      serverless.variables.getValueFromEnv.restore();
+      serverless.variables.getValueFromOptions.restore();
+      serverless.variables.getValueFromSelf.restore();
+      serverless.variables.getValueFromFile.restore();
+      serverless.variables.getValueFromCf.restore();
+      serverless.variables.getValueFromS3.restore();
+      serverless.variables.getValueFromSsm.restore();
+    });
+
+    it('should call getValueFromSls if referencing sls var', () => serverless.variables
+      .getValueFromSource('sls:instanceId').should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
+          expect(getValueFromSlsStub).to.have.been.called;
+          expect(getValueFromSlsStub).to.have.been.calledWith('sls:instanceId');
+        }));
+
+    it('should call getValueFromEnv if referencing env var', () => serverless.variables
+      .getValueFromSource('env:TEST_VAR').should.be.fulfilled
+        .then((valueToPopulate) => {
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromEnvStub).to.have.been.called;
           expect(getValueFromEnvStub).to.have.been.calledWith('env:TEST_VAR');
-        })
-        .finally(() => getValueFromEnvStub.restore());
-    });
+        }));
 
-    it('should call getValueFromOptions if referencing an option', () => {
-      const getValueFromOptionsStub = sinon
-        .stub(serverless.variables, 'getValueFromOptions')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('opt:stage').should.be.fulfilled
+    it('should call getValueFromOptions if referencing an option', () => serverless.variables
+      .getValueFromSource('opt:stage').should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromOptionsStub).to.have.been.called;
           expect(getValueFromOptionsStub).to.have.been.calledWith('opt:stage');
-        })
-        .finally(() => getValueFromOptionsStub.restore());
-    });
+        }));
 
-    it('should call getValueFromSelf if referencing from self', () => {
-      const getValueFromSelfStub = sinon.stub(serverless.variables, 'getValueFromSelf')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('self:provider').should.be.fulfilled
+    it('should call getValueFromSelf if referencing from self', () => serverless.variables
+      .getValueFromSource('self:provider').should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromSelfStub).to.have.been.called;
           expect(getValueFromSelfStub).to.have.been.calledWith('self:provider');
-        })
-        .finally(() => getValueFromSelfStub.restore());
-    });
+        }));
 
-    it('should call getValueFromFile if referencing from another file', () => {
-      const getValueFromFileStub = sinon.stub(serverless.variables, 'getValueFromFile')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('file(./config.yml)').should.be.fulfilled
+    it('should call getValueFromFile if referencing from another file', () => serverless.variables
+      .getValueFromSource('file(./config.yml)').should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromFileStub).to.have.been.called;
           expect(getValueFromFileStub).to.have.been.calledWith('file(./config.yml)');
-        })
-        .finally(() => getValueFromFileStub.restore());
-    });
+        }));
 
-    it('should call getValueFromCf if referencing CloudFormation Outputs', () => {
-      const getValueFromCfStub = sinon.stub(serverless.variables, 'getValueFromCf')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('cf:test-stack.testOutput').should.be.fulfilled
+    it('should call getValueFromCf if referencing CloudFormation Outputs', () => serverless
+      .variables.getValueFromSource('cf:test-stack.testOutput').should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromCfStub).to.have.been.called;
           expect(getValueFromCfStub).to.have.been.calledWith('cf:test-stack.testOutput');
-        })
-        .finally(() => getValueFromCfStub.restore());
-    });
+        }));
 
-    it('should call getValueFromS3 if referencing variable in S3', () => {
-      const getValueFromS3Stub = sinon.stub(serverless.variables, 'getValueFromS3')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('s3:test-bucket/path/to/key')
+    it('should call getValueFromS3 if referencing variable in S3', () => serverless.variables
+      .getValueFromSource('s3:test-bucket/path/to/key')
         .should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromS3Stub).to.have.been.called;
           expect(getValueFromS3Stub).to.have.been.calledWith('s3:test-bucket/path/to/key');
-        })
-        .finally(() => getValueFromS3Stub.restore());
-    });
+        }));
 
-    it('should call getValueFromSsm if referencing variable in SSM', () => {
-      const getValueFromSsmStub = sinon.stub(serverless.variables, 'getValueFromSsm')
-        .resolves('variableValue');
-      return serverless.variables.getValueFromSource('ssm:/test/path/to/param')
+    it('should call getValueFromSsm if referencing variable in SSM', () => serverless.variables
+      .getValueFromSource('ssm:/test/path/to/param')
         .should.be.fulfilled
         .then((valueToPopulate) => {
-          expect(valueToPopulate).to.equal('variableValue');
+          expect(valueToPopulate).to.equal(variableValue);
           expect(getValueFromSsmStub).to.have.been.called;
           expect(getValueFromSsmStub).to.have.been.calledWith('ssm:/test/path/to/param');
-        })
-        .finally(() => getValueFromSsmStub.restore());
-    });
+        }));
+
     it('should reject invalid sources', () =>
       serverless.variables.getValueFromSource('weird:source')
         .should.be.rejectedWith(serverless.classes.Error));
+
     describe('caching', () => {
       const sources = [
+        { function: 'getValueFromSls', variableString: 'sls:instanceId' },
         { function: 'getValueFromEnv', variableString: 'env:NODE_ENV' },
         { function: 'getValueFromOptions', variableString: 'opt:stage' },
         { function: 'getValueFromSelf', variableString: 'self:provider' },
@@ -1429,23 +1451,31 @@ module.exports = {
       ];
       sources.forEach((source) => {
         it(`should only call ${source.function} once, returning the cached value otherwise`, () => {
-          const value = 'variableValue';
-          const getValueFunctionStub = sinon.stub(serverless.variables, source.function)
-            .resolves(value);
+          const getValueFunctionStub = serverless.variables[source.function];
           return BbPromise.all([
-            serverless.variables.getValueFromSource(source.variableString).should.become(value),
+            serverless.variables.getValueFromSource(source.variableString)
+              .should.become(variableValue),
             BbPromise.delay(100).then(() =>
-              serverless.variables.getValueFromSource(source.variableString).should.become(value)),
+              serverless.variables.getValueFromSource(source.variableString)
+                .should.become(variableValue)),
           ]).then(() => {
             expect(getValueFunctionStub).to.have.been.calledOnce;
             expect(getValueFunctionStub).to.have.been.calledWith(source.variableString);
-          }).finally(() =>
-            getValueFunctionStub.restore());
+          });
         });
       });
     });
   });
 
+  describe('#getValueFromSls()', () => {
+    it('should get variable from Serverless Framework provided variables', () => {
+      serverless.instanceId = 12345678;
+      return serverless.variables.getValueFromSls('sls:instanceId').then((valueToPopulate) => {
+        expect(valueToPopulate).to.equal(12345678);
+      });
+    });
+  });
+
   describe('#getValueFromEnv()', () => {
     it('should get variable from environment variables', () => {
       process.env.TEST_VAR = 'someValue';
diff --git a/lib/plugins/aws/lib/naming.test.js b/lib/plugins/aws/lib/naming.test.js
index bff6a8408b7..78b86fd7e07 100644
--- a/lib/plugins/aws/lib/naming.test.js
+++ b/lib/plugins/aws/lib/naming.test.js
@@ -283,8 +283,8 @@ describe('#naming()', () => {
 
   describe('#getWebsocketsDeploymentLogicalId()', () => {
     it('should return the websockets deployment logical id', () => {
-      expect(sdk.naming.getWebsocketsDeploymentLogicalId())
-        .to.match(/WebsocketsDeployment.+/);
+      expect(sdk.naming.getWebsocketsDeploymentLogicalId(1234))
+        .to.equal('WebsocketsDeployment1234');
     });
   });
 
@@ -318,10 +318,9 @@ describe('#naming()', () => {
   });
 
   describe('#generateApiGatewayDeploymentLogicalId()', () => {
-    it('should return ApiGatewayDeployment with a date based suffix', () => {
-      expect(sdk.naming.generateApiGatewayDeploymentLogicalId()
-        .match(/ApiGatewayDeployment(.*)/).length)
-        .to.be.greaterThan(1);
+    it('should return ApiGatewayDeployment with a suffix', () => {
+      expect(sdk.naming.generateApiGatewayDeploymentLogicalId(1234))
+        .to.equal('ApiGatewayDeployment1234');
     });
   });
 
diff --git a/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js b/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js
index 767c3a8c030..01dff2d813e 100644
--- a/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js
+++ b/lib/plugins/aws/package/compile/events/websockets/lib/stage.test.js
@@ -18,7 +18,7 @@ describe('#compileStage()', () => {
     awsCompileWebsocketsEvents.websocketsApiLogicalId
       = awsCompileWebsocketsEvents.provider.naming.getWebsocketsApiLogicalId();
     awsCompileWebsocketsEvents.websocketsDeploymentLogicalId
-      = awsCompileWebsocketsEvents.provider.naming.getWebsocketsDeploymentLogicalId();
+      = awsCompileWebsocketsEvents.provider.naming.getWebsocketsDeploymentLogicalId(1234);
   });
 
   it('should create a stage resource', () => awsCompileWebsocketsEvents.compileStage().then(() => {