Repository: serverless/serverless (JavaScript)
Date: 2020-02-24 18:45:53+00:00
Task type: Feature

Issue:
Add support for VPC Endpoint associations in AWS.
# This is a Feature Proposal

## Description
Amazon API Gateway support [associating your API with an interface VPC endpoint](https://docs.aws.amazon.com/apigateway/latest/developerguide/associate-private-api-with-vpc-endpoint.html). This is supported in cloudformation via `EndpointConfiguration`. Associating your API with the VPCE creates a custom DNS alias which allows you to access your private gateway through the VPCE without the need of a `Host` header, which is necessary in certain situations, particularly if you want to make ajax calls to your private API from a browser.

What would be ideal would be to just have a `vpcEndpointIds` attribute which can be set just like `endpointType`, since both are a part of the `endpointConfiguration`. Here is a cloudformation example:
```json
"endpointConfiguration": {
        "types": [
            "PRIVATE"
        ],
        "vpcEndpointIds": [
            "vpce-0212a4ababd5b8c3e",
            "vpce-0393a628149c867ee"
        ]
    }
```



Issue discussion:
## Update
I found today that, though the aws api supports this, it has not yet been brought to cloudformation. I imagine this would happen here before there unless it's with a 3rd party plugin.
Any news on this one?
Looking at the CFT docs, looks like the VpcEndpointIds property is supported now. It was added on Nov 21, 2019

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-apigateway-restapi-endpointconfiguration.html



Solution patch:
diff --git a/docs/providers/aws/events/apigateway.md b/docs/providers/aws/events/apigateway.md
index 1e6f068a88c..27922e7c8a7 100644
--- a/docs/providers/aws/events/apigateway.md
+++ b/docs/providers/aws/events/apigateway.md
@@ -655,6 +655,24 @@ functions:
           method: get
 ```
 
+API Gateway also supports the association of VPC endpoints if you have an API Gateway REST API using the PRIVATE endpoint configuration. This feature simplifies the invocation of a private API through the generation of the following AWS Route 53 alias:
+
+```
+https://<rest_api_id>-<vpc_endpoint_id>.execute-api.<aws_region>.amazonaws.com
+```
+
+Here's an example configuration:
+
+```yml
+service: my-service
+provider:
+  name: aws
+  endpointType: PRIVATE
+  vpcEndpointIds:
+    - vpce-123
+    - vpce-456
+```
+
 ### Request Parameters
 
 To pass optional and required parameters to your functions, so you can use them in API Gateway tests and SDK generation, marking them as `true` will make them required, `false` will make them optional.
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
index c30c99c535f..385fa66b1bf 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.js
@@ -15,6 +15,7 @@ module.exports = {
     this.apiGatewayRestApiLogicalId = this.provider.naming.getRestApiLogicalId();
 
     let endpointType = 'EDGE';
+    let vpcEndpointIds;
     let BinaryMediaTypes;
     if (apiGateway.binaryMediaTypes) {
       BinaryMediaTypes = apiGateway.binaryMediaTypes;
@@ -35,6 +36,28 @@ module.exports = {
         throw new this.serverless.classes.Error(message);
       }
       endpointType = endpointType.toUpperCase();
+
+      if (this.serverless.service.provider.vpcEndpointIds) {
+        vpcEndpointIds = this.serverless.service.provider.vpcEndpointIds;
+
+        if (endpointType !== 'PRIVATE') {
+          throw new this.serverless.classes.Error(
+            'VPC endpoint IDs are only available for private APIs'
+          );
+        }
+
+        if (!Array.isArray(vpcEndpointIds)) {
+          throw new this.serverless.classes.Error('vpcEndpointIds must be an array');
+        }
+      }
+    }
+
+    const EndpointConfiguration = {
+      Types: [endpointType],
+    };
+
+    if (vpcEndpointIds) {
+      EndpointConfiguration.VpcEndpointIds = vpcEndpointIds;
     }
 
     _.merge(this.serverless.service.provider.compiledCloudFormationTemplate.Resources, {
@@ -43,9 +66,7 @@ module.exports = {
         Properties: {
           Name: this.provider.naming.getApiGatewayName(),
           BinaryMediaTypes,
-          EndpointConfiguration: {
-            Types: [endpointType],
-          },
+          EndpointConfiguration,
         },
       },
     });


Test patch:
diff --git a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
index 73ffbe6872f..8aaed26f631 100644
--- a/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
+++ b/lib/plugins/aws/package/compile/events/apiGateway/lib/restApi.test.js
@@ -169,6 +169,24 @@ describe('#compileRestApi()', () => {
     expect(() => awsCompileApigEvents.compileRestApi()).to.not.throw(Error);
   });
 
+  it('should compile if endpointType property is PRIVATE and vpcEndpointIds property is [id1]', () => {
+    awsCompileApigEvents.serverless.service.provider.endpointType = 'PRIVATE';
+    awsCompileApigEvents.serverless.service.provider.vpcEndpointIds = ['id1'];
+    expect(() => awsCompileApigEvents.compileRestApi()).to.not.throw(Error);
+  });
+
+  it('should throw error if endpointType property is PRIVATE and vpcEndpointIds property is not an array', () => {
+    awsCompileApigEvents.serverless.service.provider.endpointType = 'PRIVATE';
+    awsCompileApigEvents.serverless.service.provider.vpcEndpointIds = 'id1';
+    expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error);
+  });
+
+  it('should throw error if endpointType property is not PRIVATE and vpcEndpointIds property is [id1]', () => {
+    awsCompileApigEvents.serverless.service.provider.endpointType = 'Testing';
+    awsCompileApigEvents.serverless.service.provider.vpcEndpointIds = ['id1'];
+    expect(() => awsCompileApigEvents.compileRestApi()).to.throw(Error);
+  });
+
   it('throw error if endpointType property is not EDGE or REGIONAL', () => {
     awsCompileApigEvents.serverless.service.provider.endpointType = 'Testing';
     expect(() => awsCompileApigEvents.compileRestApi()).to.throw('endpointType must be one of');