aws / serverless-application-model

The AWS Serverless Application Model (AWS SAM) transform is a AWS CloudFormation macro that transforms SAM templates into CloudFormation templates.
https://aws.amazon.com/serverless/sam
Apache License 2.0
9.36k stars 2.39k forks source link

Sharing a single API dns name across varying base path names that represent API extensions #2175

Open bmckinle opened 3 years ago

bmckinle commented 3 years ago

Describe your idea/feature/enhancement

We need a way to extend an API across different repositories by creating a new base path name for an existing, shared API DNS name. For example:

api.mycompany.com/service1 created in template1.yaml using a single API Gateway and Serverless Function resources. api.mycompany.com/service2 created in template2.yaml using the template1.yaml API Gateway and different Serverless Function resources.

Proposal

Our understanding is that is a limitation of SAM but using a combination of CloudFormation and SAM can achieve automation that would achieve a shared api domain goal that supports extensions via basename changes only. This should be supportable via SAM itself.

Things to consider:

Sharing API elements across SAM templates. We are primarily looking ways to simplify maintaining a growing API over time without resorting to creating a unique domain name for each API extension that emerges from collaborating teams over time. Ostensibly, service1.mycompany.com service2.mycompany.com ... is not as valuable as api.mycompany.com/service1 api.mycompany.com/service2 ...

Additional Details

AWS Support suggested we create this issue as the limitation of sharing a single API domain across SAM templates is a known limitation.

hawflau commented 3 years ago

@bmckinle Thanks for your proposal. Yeah, it's a limitation that SAM doesn't support sharing API resource across multiple stacks seamlessly.

We've been thinking a lot about how we can solve that. We posted a workaround in a previous thread. I was using a root stack and nested stack to illustrate in that example but I guess it is solving the problem as yours. Can you take a look and give us feedback? This will help us formulate a better solution.

bmckinle commented 3 years ago

Referring to the workaround, while solution 1 heads in the right direction, it suggests we need to create a single API (openapi/swagger). Yet, we need multiple APIs managed in decoupled (multi-team) manner. Solution 2 removes openapi/swagger altogether, which is something we want to avoid entirely. We want multiple openapi.yaml and template.yaml module/api sets that links to a common dns_domain_template.yaml. Here are two examples of this type of architecture, with the first from AWS blogs and the second a third party vendor:

  1. AWS Microservices Blog Notice the following multi-api-gateways within one domain is supported for a single or central account: my-custom-domain.com/path1 my-custom-domain.com/path2 Can this be achieved in cloudformation alone? How? Can this be supported in multiple accounts, such as dev, qa, and production?

  2. Serverless This is supported out of the box. Why can't SAM emulate what serverless is doing? What blocks SAM from achieving the same?

In summary, we need a way to use SAM to deploy multiple API Gateways that share a domain name with each gateway using different base paths.

troycampano commented 1 year ago

@bmckinle I've been able to achieve this URL format using multiple SAM projects:

api.mycompany.com/service1
api.mycompany.com/service2

I used AWS::ApiGateway::BasePathMapping to associate various paths to my custom domain used in API Gateway. It looks like this:

  Microservice02Mapping:
    Type: 'AWS::ApiGateway::BasePathMapping'
    Properties:
      BasePath: 'app02'
      DomainName: api.hostname.com
      RestApiId: !Ref ApiGatewayApi
      Stage: Prod

I have mappings like this in each of my SAM projects. In the API Gateway console, under "Custom domain names", the "API mappings" tab looks like this:

Screenshot 2022-12-06 220123

I posted more details here: https://github.com/aws/serverless-application-model/discussions/2703#discussioncomment-4328858

ricott commented 1 year ago

@troycampano We use this approach extensively and it works ok. We use it for the HTTP API Gateway. It took a session with an AWS serverless specialist SA to come up with the solution a year ago when we adopted the pattern.

  ApiGatewayMapping:
    Type: AWS::ApiGatewayV2::ApiMapping
    DependsOn: Gateway
    Properties:
      ApiId: !Ref Gateway
      ApiMappingKey: !If [IsFeatureDeployment, !Sub '${AWS::StackName}/users', 'users']
      DomainName: !FindInMap [DeploymentEnvironment, !Ref Environment, DomainName]
      Stage: !Ref Gateway.Stage

You get a lot of API Gateways but at least it solves the very basic problem of being forced to build a single monolithic API stack. With this approach, you can build and deploy multiple stacks independently and still use the same custom domain name, e.g. api.mycompany.com.

There is a problem though with paths. Imagine you have a users endpoint as per the example above. Gateway path mapping points to /users. Since users is now part of base path there is a problem with the path property of the individual serverless functions. If you want to expose a get with /users/{user_id} and a get with /users where you for instance want to allow a lookup via email - then this is not possible using this approach.

This works

      Events:
        GetUser:
          Type: HttpApi
          Properties:
            ApiId: !Ref Gateway
            Method: get
            Path: /{user_id}
            Auth:
              Authorizer: OAuth2Authorizer
              AuthorizationScopes:
                - "read:users"

This works but looks awful since the path has to be /users/?email=abc@xyz.se with the ending /.

        GetUsers:
          Type: HttpApi
          Properties:
            ApiId: !Ref Gateway
            Method: get
            Path: /
            Auth:
              Authorizer: OAuth2Authorizer
              AuthorizationScopes:
                - "read:users"

This means you have to add some artificial API mapping path like this, which we currently use ApiMappingKey: !If [IsFeatureDeployment, !Sub '${AWS::StackName}/u/v1', 'u/v1']

Then for the serverless endpoints, you can use Path: /users/{user_id} and /users

Which results in these endpoints

get api.mycompany.com/u/v1/users/{user_id}
get api.mycompany.com/u/v1/users?email=

If we could import and share an API Gateway instance instead and only attach the serverless functions we wouldn't have this problem. My understanding is that the serverless framework supports this but I haven't gotten around to trying it yet.

bmckinle commented 1 year ago

We've switch to AWS CDK over the last year, and using a BasePathMapping object and existing apigateway.from... existing domain, are able to add a new base path to an existing domain quite easily. Here's a snippet of how easy it is:

       #
        # Create api gateway for proxy integration with lambda
        #
        existing_domain_name = apigateway.DomainName.from_domain_name_attributes(self,
                                                                                 "Company API Domain",
                                                                                 domain_name=api_domain_name,
                                                                                 domain_name_alias_hosted_zone_id=hosted_zone_id,
                                                                                 domain_name_alias_target=api_gateway_domain_name)

        rest_api = apigateway.RestApi(self, "ipset_refresher-api",
                                      rest_api_name="My REST API Service",
                                      description=f"This service updates ipset.yaml on {target_s3_bucket_name}",
                                      deploy_options=apigateway.StageOptions(stage_name=stage_name))

        apigateway.BasePathMapping(self,
                                   "MyBasePathMapping",
                                   domain_name=existing_domain_name,
                                   rest_api=rest_api,
                                   base_path=base_path
                                   )

The world moves on.

hoffa commented 1 year ago

Potentially relevant: https://github.com/aws/serverless-application-model/discussions/2703#discussioncomment-4328858