Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: custom domain name creation for websocket APIs #228

Closed
wants to merge 82 commits into from

Conversation

pavel-ignatiev
Copy link

@pavel-ignatiev pavel-ignatiev commented May 21, 2019

Fixes #207

Description of Issue Fixed
This feature enhances the plugin with following functionality:

  • create a custom domain name using AWS API Gateway V2
  • setup an API mapping between the regional domain name and the websocket API
  • create correspondent AWS Route53 resource records to map the custom domain onto the regional domain name

From the user perspective, it allows you to add an independent Serverless configuration for a second custom domain which will be mapped on the deployed websocket API (see also modified README.md in the head repository).

Here is an example of the new configuration:

custom:
  customDomain:
    domainName: serverless.foo.com
    stage: ci
    basePath: api
    certificateName: '*.foo.com'
    createRoute53Record: true
    endpointType: 'regional'
    websockets:
      domainName: serverless.foo.com
      stage: ci
      certificateName: '*.foo.com'
      createRoute53Record: true
      endpointType: 'regional'

As stated in the README.md, the websockets part can be omitted entirely here but if it is present, the property websockets.domainName has to be set.

The property websockets.endpointType can be set to arbitrary value but the regional endpoint type is enforced in the code due to missing AWS support for EDGE optimized websocket endpoints.

As far as I'm concerned, this isn't possible to resolve both HTTP and websocket requests on a DNS level for the same custom domain, therefore the both custom domain names have to be explicitly different.

Changes proposed in this pull request:

  1. New wrapper class DomainInfoWs for the custom domain information provided by AWS API Gateway V2

    • the API key naming notation is different than that of API Gateway V1 (UpperCamelCase)
    • the API returns an object nested in an array: [{}], which breaks backward compatibility with the original class DomainInfo
  2. Modifications in types.ts

    • the interface ServerlessInstance is extended by the nested object service.custom.customDomain.websockets
    • AWS API Gateway V2 is referenced in the same interface by providers.aws.sdk.ApiGatewayV2 property (note the spelling here!)
  3. Modifications in index.ts

    • the class DomainInfoWs is imported
    • the class ServerlessCustomDomain is extended by properties which are specific for websocket custom domain name creation - reusing existing properties could be possible in some cases (ACM) but this would break backward compatibility in a general case (EDGE vs regional endpoints etc.)
    • all lifecycle functions are replaced with their new versions which take into account both custom domains if enabled; old lifecycle functions are reused here to create a REST custom domain
    • the function initializeVariables checks which of both custom domains are enabled; if at least one custom domain is enabled, Route53 and CloudFormation client instances are created; the remaining variables are initialized separately for both custom domains if they are enabled respectively
    • initializeVariables shows a CLI message (besides of Serverless error thrown by another function) if a custom domain is enabled but no domain name is provided in the configuration
    • following functions were replicated and adapted to the websocket case:
      • evaluateEnabled => evaluateEnabledWs
      • printDomainSummary => printDomainSummaryWs
      • createDomain => createDomainWs
      • getDomainInfo => getDomainInfoWs
      • getCertArn => getCertArnWs
      • createCustomDomain => createCustomDomainWs
      • changeResourceRecordSet => changeResourceRecordSetWs
      • deleteDomain => deleteDomainWs
      • deleteCustomDomain => deleteCustomDomainWs
      • addOutputs => addOutputsWs (still not working but not critical for the domain creation)
      • getRoute53HostedZoneId => getRoute53HostedZoneIdWs
    • in some cases the original functions could be theoretically reused by providing domainType and domainName as arguments, which I didn't implement in this PR
    • following functions were added to the class:
      • getWssApiId to retrieve the websocket API ID from CloudFormation
      • getApiMappingWs looks for existing API mappings for a given domain name and websocket API ID
      • createApiMappingWs creates new API mapping using API Gateway V2 for a given domain name
      • updateApiMappingWs updates an existing API mapping if found by getApiMappingWs
      • deleteApiMappingWs deletes an existing API mapping as a part of removeMappings lifecycle function
      • setupApiMappingWs creates a new or updates an existing API as a part of setupMappings lifecycle function
    • both apigatewayv2.createApiMapping() and apigatewayv2.updateApiMapping accept ApiMappingKey as an optional parameter. I didn't find any sensible documentation about this parameter before and thus it is currently hardcoded as an empty string
    • printDomainSummaryWs additionally prints the Hosted Zone Id of the websocket domain by analogy with Add more information to domain summary #230, the summaries for both REST and websocket domains are separated by "---" string now
  4. Unit tests (index.test.ts)

    • Added a reference to ApiGatewayV2 in constructPlugin function
    • extended constructPlugin.serverless.service.custom.customDomain by a nested websockets property
    • extended the arguments of each constructPlugin call to contain an empty websockets: {} object
    • Following unit tests were added:
      • unsupported websocket endpoint type
      • create, update and delete websocket API mapping
      • get certificate ARN / name for a websocket domain
      • create a domain name for a websocket endpoint
      • omit Route53 record if configured
      • create new A / AAAA record for a websocket domain
      • get existing websocket API mapping
      • get API mapping ID for a given websocket API ID
      • get websocket API ID from CloudFormation stack
      • delete websocket domain name incl. finding the domain name and deleting Route53 record
      • Ambiguous enablement boolean for the websocket domain
      • Fail websocket domain name creation if no domainName initialized in customDomain.websockets
      • Websocket endpoint type should be reset to regional by default even if not configured at all
      • If a websocket stage is not given, it is set to test

… empty websockets structure as an additional parameter (via constructPlugin)
…tom domain and a corresponding Route53 record
…tom domain and a corresponding Route53 record
…he websocket domain and extended 'summary printing' test to cover the function again
@captainsidd
Copy link
Contributor

Hi @pavel-ignatiev ! This is awesome, thanks for doing all this work. It'll take us a bit to review your PR, so stay tuned.

@daveisfera
Copy link

Any update on when this will be merged and released?

@pavel-ignatiev
Copy link
Author

pavel-ignatiev commented Jul 4, 2019

Hi @captainsidd, I think the last Travis build is frozen - could you please retrigger the build? Alternatively I can commit the new changelog which would also retrigger the build, but I think this should be done after review.

@aligg73
Copy link

aligg73 commented Jul 6, 2019

Hi Pavel, this is awesome. I deployed your version your version and it works flawlessly. I have a suggestion for improvement, although I'm not sure it belongs here:

when I define an import for WebsocketApi like this in the serverless.yml (which prior to this has been exported from another websockets stack):
WebsocketsApi:
'Fn::ImportValue': ${self:custom.stage}-WebsocketsApi

The generated CloudFormation update template doesn't include this import value but just uses 'WebsocketsApi', which will cause CF to create a new domain upon deploy for that particular stack.

This is troublesome, as just like with REST APIs (see: https://serverless-stack.com/chapters/api-gateway-domains-across-services.html) it would be good if several WS stacks can re-use the same auto-generated domain, which the custom domain(s) can then point to.

Is this behaviour coming from your PR or something in CF that hasn't been resolved by AWS yet?
Thanks.

@pavel-ignatiev
Copy link
Author

Hi @aligg73 , nice to see the feature is working!

Regarding the websocketsApi property - as far as I'm concerned, the reuse of an existing websocket API has yet to be implemented. I surely didn't address this in the PR because we didn't have such use case in the dev yet, but it's good to know that the property is actually useful and not obsolete.

I'll try to reproduce the problem in my env and will write back then. This would definitely take more than one week :)

@aligg73
Copy link

aligg73 commented Jul 6, 2019 via email

@aat2703
Copy link

aat2703 commented Jul 12, 2019

Great feature request! Really looking forward to it!

Copy link
Contributor

@captainsidd captainsidd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi there!

Sorry for taking so long with reviewing this, we've been focusing our energies on more internal facing work.

We appreciate the massive amount of work this was, but we'd like for some changes to be made before we merge this in. The two big areas to focus on are 1) listing different custom domain configurations rather than having websockets be a subsection, and 2) DRY-ing out the code. Also, this is a major version change, as it's a significant new feature and would break existing serverless.yml configs if customDomain now required a list (although could make that backwards compatible!).

Thanks again for undertaking this, it's always great to see community members pitch in when we don't have the bandwidth to implement new features.

certificateName: '*.foo.com'
createRoute53Record: true
endpointType: 'regional'
websockets:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way that we could just have customDomain take in a list of domain name configurations rather than have a separate config section just for websockets? Could also help with closing #88 if implemented correctly, too.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is surely a more general solution. Smth like that will be backward compatible if the plugin assumes websocket: false when omitted in the config:

custom:
  customDomain:
     - domainName: serverless.foo.com
        stage: ci
        basePath: api
        certificateName: '*.foo.com'
        createRoute53Record: true
        endpointType: 'regional'
     - domainName: serverless-ws.foo.com
        stage: ci
        certificateName: '*.foo.com'
        createRoute53Record: true
        endpointType: 'regional'
        websocket: true

What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is along the lines of we were thinking.


let credentials;

if (this.enabled || this.enabledWs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpicking here, but not a fan of the naming convention something and somethingWs - maybe specify that something refers to rest api endpoints as a direct contrast with somethingWs?

* If the property's value is provided, this should be boolean, otherwise an exception is thrown.
* If no customDomain.websockets object exists, websocket creation is disabled.
*/
public evaluateEnabledWs(): boolean {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's a lot of the reasoning behind why having a list of domain configurations might be better. There's little functional difference in this function as opposed to evaluateEnabled() - it just searches for the enabled parameter in the websocket section. This is repeated throughout the PR, and having a list of domain configs would make the code more DRY.

/**
* Returns information about custom domain name registered in ApiGatewayV2
*/
public async getDomainInfoWs(): Promise<DomainInfoWs> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing in this function as above.

Maybe try consider having the actual API call to ApiGatewayV2 be abstracted out? i.e. have a function that takes in the endpoint type [rest vs websocket] and possibly the AWS resource [apigateway vs apigatewayv2], and function call [getDomainName]. It would cut down on the duplication of functions that largely did the same thing, just made different api calls.

@@ -1,6 +1,6 @@
{
"name": "serverless-domain-manager",
"version": "3.2.6",
"version": "3.3.0",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely a major version bump, this is a 4.0.0 change. This is a huge lift, thanks for undertaking it.

@pavel-ignatiev
Copy link
Author

pavel-ignatiev commented Jul 17, 2019

Hi @captainsidd , thanks for the insightful review! I purposefully ignored the DRY aspect in the first PR iteration in order to preserve existing test functionality, but more coherent code would be very nice for sure. I'll try to reconsider the config structure at first and will get in touch with you a little later.

Update: I'm working on my thesis now, thus the refactoring will take some time...

@aat2703
Copy link

aat2703 commented Aug 7, 2019

Anything we can do to help with this issue? 👍

@kenpachiii
Copy link

Would anyone here be opposed to me making the necessary changes? My plan was to just fork the work @pavel-ignatiev has done, and make the changes requested by @captainsidd. Which would be

  1. have customDomain take in a list of configs
  2. Refactor naming convention of enabled vs enabledWs
  3. DRY out the code, particularly around evaluateEnabled() / evaluateEnabledWs() and getDomainInfo() / getDomainInfoWs().

I can knock it out in no time so just me know. Thanks!

@pavel-ignatiev
Copy link
Author

@kenpachiii Absolutely no objections here, this would be extremely helpful!

@kenpachiii kenpachiii mentioned this pull request Sep 5, 2019
@kenpachiii
Copy link

I made the changes. Plus some others here. #250
A review so we can get this merged would be much appreciated!

@sapher
Copy link

sapher commented Nov 28, 2019

No update ?

@jan-wilhelm
Copy link

Can we help with the review? Really looking forward to seeing this get merged

@claudio-petrini
Copy link

Any update on this, really looking forward to use it :)

@aoskotsky-amplify
Copy link
Member

I'm closing this PR because I believe it's been implemented with #319. If you feel this PR is still needed then feel free to reopen it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support Websockets api
10 participants