Repository: serverless/serverless (JavaScript)
Date: 2020-04-24 18:42:41+00:00
Task type: Feature

Issue:
AWS: Extend ${ssm} syntax to get output from another region
# Feature Proposal

## Description

1. It is sometimes useful to deploy serverless to one AWS region, configuring it with an SSM value read from another region. For example, a serverless project using Lambda@Edge must be deployed in us-east-1, even if it is mainly interfacing with infrastructure and configuration defined in a different region. If we extend the ssm variable syntax to optionally specify the region, then it would be much easier to reference ssm values in another region.
1. We can extend the ssm variable syntax to match the cf syntax: `${ssm.REGION:key}`, e.g. `${ssm.us-east-1:/servers/blue/url}`. If region is omitted, default region is used, as same as current `${ssm}`.
1. While this feature could be implemented by a plugin, it seems more natural to include it in the core serverless platform. It improves the similarity cf and ssm variable syntax, which should work towards making the serverless.yml easier for developers to use.

Similar or dependent issues:

- This change is similar to https://github.com/serverless/serverless/pull/5579




Issue discussion:
@joshuanapoli thanks for report. We're open for PR that implements that

Solution patch:
diff --git a/docs/providers/aws/guide/variables.md b/docs/providers/aws/guide/variables.md
index 9a95d2e38cd..6c38984922c 100644
--- a/docs/providers/aws/guide/variables.md
+++ b/docs/providers/aws/guide/variables.md
@@ -265,6 +265,7 @@ functions:
 ```
 
 In the above example, the value for `myKey` in the `myBucket` S3 bucket will be looked up and used to populate the variable.
+Buckets from all regions can be used without any additional specification due to AWS S3 global strategy.
 
 ## Reference Variables using the SSM Parameter Store
 
@@ -310,6 +311,18 @@ custom:
   myArrayVar: ${ssm:/path/to/stringlistparam~split}
 ```
 
+You can also reference SSM Parameters in another region with the `ssm.REGION:/path/to/param` syntax. For example:
+
+```yml
+service: ${ssm.us-west-2:/path/to/service/id}-service
+provider:
+  name: aws
+functions:
+  hello:
+    name: ${ssm.ap-northeast-1:/path/to/service/myParam}-hello
+    handler: handler.hello
+```
+
 ## Reference Variables using AWS Secrets Manager
 
 Variables in [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) can be referenced [using SSM](https://docs.aws.amazon.com/systems-manager/latest/userguide/integration-ps-secretsmanager.html). Use the `ssm:/aws/reference/secretsmanager/secret_ID_in_Secrets_Manager~true` syntax(note `~true` as secrets are always encrypted). For example:
diff --git a/lib/classes/Variables.js b/lib/classes/Variables.js
index 4b47521a021..9a7994d0c89 100644
--- a/lib/classes/Variables.js
+++ b/lib/classes/Variables.js
@@ -54,9 +54,11 @@ class Variables {
     this.optRefSyntax = RegExp(/^opt:/g);
     this.selfRefSyntax = RegExp(/^self:/g);
     this.stringRefSyntax = RegExp(/(?:('|").*?\1)/g);
-    this.cfRefSyntax = RegExp(/^(?:\${)?cf(\.[a-zA-Z0-9-]+)?:/g);
     this.s3RefSyntax = RegExp(/^(?:\${)?s3:(.+?)\/(.+)$/);
-    this.ssmRefSyntax = RegExp(/^(?:\${)?ssm:([a-zA-Z0-9_.\-/]+)[~]?(true|false|split)?/);
+    this.cfRefSyntax = RegExp(/^(?:\${)?cf(?:\.([a-zA-Z0-9-]+))?:(.+?)\.(.+)$/);
+    this.ssmRefSyntax = RegExp(
+      /^(?:\${)?ssm(?:\.([a-zA-Z0-9-]+))?:([a-zA-Z0-9_.\-/]+)[~]?(true|false|split)?/
+    );
     this.strToBoolRefSyntax = RegExp(/^(?:\${)?strToBool\(([a-zA-Z0-9_.\-/]+)\)/);
 
     this.variableResolvers = [
@@ -742,18 +744,27 @@ class Variables {
     return BbPromise.resolve(valueToPopulate);
   }
 
-  getValueFromCf(variableString) {
-    const regionSuffix = variableString.split(':')[0].split('.')[1];
-    const variableStringWithoutSource = variableString.split(':')[1].split('.');
-    const stackName = variableStringWithoutSource[0];
-    const outputLogicalId = variableStringWithoutSource[1];
+  buildOptions(regionSuffix) {
     const options = { useCache: true };
     if (!_.isUndefined(regionSuffix)) {
       options.region = regionSuffix;
     }
+    return options;
+  }
+
+  getValueFromCf(variableString) {
+    const groups = variableString.match(this.cfRefSyntax);
+    const regionSuffix = groups[1];
+    const stackName = groups[2];
+    const outputLogicalId = groups[3];
     return this.serverless
       .getProvider('aws')
-      .request('CloudFormation', 'describeStacks', { StackName: stackName }, options)
+      .request(
+        'CloudFormation',
+        'describeStacks',
+        { StackName: stackName },
+        this.buildOptions(regionSuffix)
+      )
       .then(result => {
         const outputs = result.Stacks[0].Outputs;
         const output = outputs.find(x => x.OutputKey === outputLogicalId);
@@ -794,9 +805,10 @@ class Variables {
 
   getValueFromSsm(variableString) {
     const groups = variableString.match(this.ssmRefSyntax);
-    const param = groups[1];
-    const decrypt = groups[2] === 'true';
-    const split = groups[2] === 'split';
+    const regionSuffix = groups[1];
+    const param = groups[2];
+    const decrypt = groups[3] === 'true';
+    const split = groups[3] === 'split';
     return this.serverless
       .getProvider('aws')
       .request(
@@ -806,7 +818,7 @@ class Variables {
           Name: param,
           WithDecryption: decrypt,
         },
-        { useCache: true }
+        this.buildOptions(regionSuffix)
       ) // Use request cache
       .then(
         response => {


Test patch:
diff --git a/lib/classes/Variables.test.js b/lib/classes/Variables.test.js
index c58e4118b03..17b8266029a 100644
--- a/lib/classes/Variables.test.js
+++ b/lib/classes/Variables.test.js
@@ -2159,6 +2159,27 @@ module.exports = {
         })
         .finally(() => ssmStub.restore());
     });
+    it('should get variable from Ssm of different region', () => {
+      const ssmStub = sinon
+        .stub(awsProvider, 'request')
+        .callsFake(() => BbPromise.resolve(awsResponseMock));
+      return serverless.variables
+        .getValueFromSsm(`ssm.us-east-1:${param}`)
+        .should.become(value)
+        .then(() => {
+          expect(ssmStub).to.have.been.calledOnce;
+          expect(ssmStub).to.have.been.calledWithExactly(
+            'SSM',
+            'getParameter',
+            {
+              Name: param,
+              WithDecryption: false,
+            },
+            { region: 'us-east-1', useCache: true }
+          );
+        })
+        .finally(() => ssmStub.restore());
+    });
     it('should get variable from Ssm using path-style param', () => {
       const ssmStub = sinon
         .stub(awsProvider, 'request')