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

API Gateway Custom Domain Names #783

Closed
brettstack opened this issue Jan 25, 2019 · 41 comments
Closed

API Gateway Custom Domain Names #783

brettstack opened this issue Jan 25, 2019 · 41 comments

Comments

@brettstack
Copy link
Contributor

brettstack commented Jan 25, 2019

SAM Input:

MyApiSimpleDomain:
  Type: AWS::Serverless::Api
  Properties:
    ...
    EndpointConfiguration: REGIONAL
    # Simple usecase - specify just the Domain Name and we create the rest using sane defaults.
    # A cert is created as well as a base path mapping from '/' to {MyApi.StageName}
    # The `EndpointConfiguration` of the API is used (or default to EDGE)
    Domain: example.com
    
    # The above expands into:
    # Domain:
    #   DomainName: example.com
    # 
    #   # TODO: Should ConfigureRoute53 be on by default? This is useful if
    #   # Route53 is your domain provider and you don't want to manage the domain
    #   # records through some other means (e.g console/manually; separate stack)
    #   Route53: true
    # 
    #   EndpointConfiguration: REGIONAL # {MyApiSimpleDomain.EndpointConfiguration}
    #   Certificate: !Ref MyApiSimpleDomainCertificate # Automatically created
    #   BasePath: /


MyApiAdvancedDomain:
  Type: AWS::Serverless::Api
  Properties:
    ...
    Domain:
      # Domain accepts either a string (DomainName), an Object, or an Array of Objects
      - DomainName: example.com # Required
      
        # Set this to false to create the Route53 (or your domain provider) records yourself
        Route53:
          EvaluateTargetHealth: true # Default to false

        # TODO: What happens when this isn't the same as the API's EndpointConfiguration?
        # Allowed values: REGIONAL, EDGE; defaults to MyApi.EndpointConfiguration
        EndpointConfiguration: REGIONAL
        
        # If `EndpointConfiguration` is REGIONAL, this needs to be an ACM cert created in the same region as the API
        # If `EndpointConfiguration` is EDGE, this needs to be an ACM cert created in IAD
        # SAM doesn't need to perform any additional validation here
        # Default: An ACM Certificate is created for you
        Certificate: arn:...

        # Advanced Certificate:
        Certificate:
          CertificateArn: !Ref MyCertificate
          ValidationMethod: DNS # Default: {Route53 == true ? DNS : EMAIL}; Allowed: EMAIL|DNS

        # Default: '/' which creates a BasePathMapping from the root of the domain
        # to this API's StageName
        BasePath: /api

        # Either BasePathMappings OR BasePath should be specified; if both are specified an error should be thrown.
        # Including here to advanced configuration
        BasePathMappings:
          - BasePath: /api # Default: '/'
            Api: !Ref MyApiAdvancedDomain # Default: !Ref MyApiAdvancedDomain; use case: mapping this domain to additional APIs
            Stage: '' # Default: {MyApi.Stage}
            # Setting `Stage` to '' sets up a mapping which allows clients to specify
            # the Stage in the path when making requests.
            # e.g http://example.com/api/Prod/users, http://example.com/api/Beta/users
          - BasePath: /other-api
            Api: !Ref MyOtherApi # Add a Domain mapping which points to a different API+Stage

CloudFormation Output:

Resources:
  ...
  # This is only created if `Domain.Certificate` isn't provided
  MyApiCertificate:
    Type: 'AWS::CertificateManager::Certificate'
    Properties:
      DomainName: example.com
  
  MyApiDomainName:
    Type: 'AWS::ApiGateway::DomainName'
    Properties:
      # If `Domain.EndpointConfiguration` is 'REGIONAL', set `RegionalCertificateArn` instead of `CertificateArn`
      # If `Domain.Certificate` is provided, the value gets passed through instead of creating `MyApiCertificate`
      CertificateArn: !Ref MyApiCertificate

      DomainName: example.com
  
  # Create one `BasePathMapping` Resource per `Domain.BasePathMappings`
  MyApiBasePathMapping:
    Type: 'AWS::ApiGateway::BasePathMapping'
    Properties:
      RestApiId: !Ref MyApi
      DomainName: !Ref MyApiDomainName
      BasePath: /
      Stage: Prod
  
  MyApi:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      ...

  MyApiRoute53RecordSetGroup:
    Type: AWS::Route53::RecordSetGroup
    Properties:
      HostedZoneName: example.com.
      RecordSets:
        - Name: example.com.
          Type: A
          AliasTarget:
            EvaluateTargetHealth: false
            HostedZoneId: !GetAtt MyApiDomainName.DistributionHostedZoneId
            DNSName: !GetAtt MyApiDomainName.DistributionDomainName
            
            # For REGIONAL:
            # HostedZoneId: !GetAtt MyApiDomainName.RegionalHostedZoneId
            # DNSName: !GetAtt MyApiDomainName.RegionalDomainName

Related Issues

@hoegertn
Copy link

The Route53 record would be:

  MyRoute53Record:
    Type: 'AWS::Route53::RecordSet'
    Properties:
      HostedZoneName: example.com.
      Name: example.com.
      Type: A
      AliasTarget:
        HostedZoneId: !GetAtt MyApiDomainName.DistributionHostedZoneId
        DNSName: !GetAtt MyApiDomainName.DistributionDomainName

@timoschilling
Copy link
Contributor

Feature request:

Domain:
  IPv6: true

Which creates an AWS::Route53::RecordSet with Type: A and one with Type: AAAA

@brettstack
Copy link
Contributor Author

@hoegertn @timoschilling Thanks. We should probably use AWS::Route53::RecordSetGroup to allow for multiple RecordSets. If IPv6 is a Route53 level configuration only, we should rename ConfigureRoute53 to Route53 and allow either an Object accepting properties such as IPv6, or a boolean with sane defaults (default to IPv6: true?).

@brettstack
Copy link
Contributor Author

I imagine most users want to have a single domain mapped to a single API. If that's the case, then leaving it as a single Domain is fine. However, if a significant number of users are likely to map multiple Domains to a single API+Stage, we should consider allowing a list of Domains. We can always launch initially with a single Domain and modify Domain to also accept an Array in the future. We just need to think this through to make sure we don't go through any one-way doors that would be backwards incompatible.

@brettstack
Copy link
Contributor Author

@timoschilling Are you referring to the scenario where you specify REGIONAL, then create your own CloudFront Distribution using IPv6 and point Route53 at that CF Distro? IPv6 aside, we should aim to cover the common use case of self-managed CF Distros.

The main question to answer here is: does that only have ramifications for Domain Name/Route53 (e.g Route53 now needs to point at your CF Distro with your CF Distro pointing at your API endpoint) or does it affect other areas of your API? If it's just Route53, we can add add a HostedZoneId and DNSName property to the Route53 section which takes an ARN and then creates RecordSets based using those values if defined (and uses AAAA if IPv6 is defined). Alternatively we can allow setting CloudFrontDistribution and we set DNSName and HostedZoneId based on !GetAtt of CloudFrontDistribution but this isn't as flexible (e.g when the CF Distro was created external to this template and you can't get a reference to the resource)

@ericofusco
Copy link

How would the certificate validation work? Can we get the cloudformation output from Certificate resource and add to Route53 or using email is a better option to implement on this flow?

@deleugpn
Copy link

deleugpn commented Feb 1, 2019

Would love domain on SAM. I'm also curious as to how certificate validation would be handled since that's the biggest issue with AWS at the moment because of how hard it is to automate.

@brettstack
Copy link
Contributor Author

It's been a while since I've done cert creation via CFN. Does the stack stay in CREATING process until you approve the email regarding certificate creation? Email was the only method I was aware of for validation. Any solution needs to also work for other DNS providers.

@deleugpn
Copy link

deleugpn commented Feb 4, 2019

AFAIK it does stay in creating for up to 24 hours.
There's a new validation method for DNS lookup but it requires calling describeStack to see what value should be created on Route 53, which I don't think it's feasible for CFN.

@brettstack
Copy link
Contributor Author

Some additional (likely less common) scenarios:

  1. Single Domain pointing at multiple APIs
  2. Multiple Domains pointing at single API
  3. Multiple Domains pointing at multiple APIs

Proposal summary: We can allow Domain to also accept an Array for specifying multiple Domains. To point a Domain at additional APIs, the user specifies Api in the BasePathMapping (by default, it uses !Ref ThisApi). The syntax is a little odd since you're referencing a separate API from a config nested under a different API, but I think acceptable given that this is probably less common than the 1:1 api:domain use case, and this syntax is better than the alternative which would be splitting Domain into a separate resource.

Note that we may choose not to support these cases on the initial release of Custom Domain Name support, but want to leave the syntax open to allow for it in the future.

# Single Domain pointing at multiple APIs
SingleDomainApi1:
  Type: AWS::Serverless::Api
  Properties:
    ...
    Domain:
      DomainName: example.com
      Route53:
        EvaluateTargetHealth: true
      EndpointConfiguration: REGIONAL
      Certificate: arn:...
      BasePathMappings:
        - BasePath: /api
          Stage: ''
        - BasePath: /api
          Stage: ''

          # Feels odd because this will add a mapping of the Domain
          # (which we're configuring under Api1) to point to Api2
          # This is probably the simplest implementation in terms
          # of development, and while a little odd, is the smallest
          # amount of additional work required by end user
          Api: !Ref SingleDomainApi2

SingleDomainApi2:
  Type: AWS::Serverless::Api
  Properties:
    ...

# Multiple Domains pointing at single API
MultipleDomainsApi:
  Type: AWS::Serverless::Api
  Properties:
    ...
    # Domain: [example.com, foobar.example.com]
    Domain:
      - DomainName: example.com
        Route53:
          EvaluateTargetHealth: true
        EndpointConfiguration: REGIONAL
        Certificate: arn:...
        BasePathMappings:
          - BasePath: /api
            Stage: ''
      - DomainName: foo.example.com
        ...

# Multiple Domains pointing at multiple APIs
MultiDomainMultiApi1:
  Type: AWS::Serverless::Api
  Properties:
    ...
    Domain:
      - DomainName: example.com
        Route53:
          EvaluateTargetHealth: true
        EndpointConfiguration: REGIONAL
        Certificate: arn:...
        BasePathMappings:
          - BasePath: /api
            Stage: ''
          - BasePath: /api
            Stage: ''
            Api: !Ref MultiDomainMultiApi2
      - DomainName: foo.example.com
        ... # Specify multiple BasePathMappings similar to above

  MultiDomainMultiApi2:
    Type: AWS::Serverless::Api
    Properties:
      ...

@brettstack
Copy link
Contributor Author

There's a new validation method for DNS lookup but it requires calling describeStack to see what value should be created on Route 53, which I don't think it's feasible for CFN.

@deleugpn I was hoping we'd be able to simply just add a CNAME to Route53 within the same template and that would complete the validation process and let the stack creation complete. Have you tried this/will this not work?

@brettstack
Copy link
Contributor Author

If adding DNS validation is viable, we should accept an Object for the Certificate property:

Certificate:
    CertificateArn: !Ref ...
    ValidationMethod: EMAIL|DNS # Default: {Route53 == true ? DNS : EMAIL}

Specifying Certificate: !Ref ... should also still work

@txase
Copy link
Contributor

txase commented Feb 6, 2019

Another use case that should be considered and tested: I have a production environment and N dev/test environments. I don't want to provision a custom domain for my dev/test environments.

The way we make this work in Stackery is to allow users to pass in the domain as a parameter. We then check the value of the parameter, and if it matches a specific value ('false' in our case), then we don't provision the custom domain resources.

This is an important bit of functionality for larger teams and organizations that need custom domains for prod but don't want to deal with the complexity and governance overhead of maintaining custom domains for all sorts of other environments.

@brettstack
Copy link
Contributor Author

@txase are you referring to Condition support? We can certainly add that; good call out.

@txase
Copy link
Contributor

txase commented Feb 6, 2019

Yes, that's the largest part of it. But it also requires specifying a sentinel value to test against (e.g. 'false'). Unfortunately, you can't use AWS::NoValue because that can't be passed in as a CloudFormation parameter from SSM Parameter Store.

@deleugpn
Copy link

deleugpn commented Feb 6, 2019

@brettstack it's about whether you're taking a certificate as parameter or trying to create it within the template. It's not about the Route 53 Domain.
I think the only option here will be to take in a Certificate ARN as parameter. If you try to create a Certificate from CloudFormation itself, you have two choices: Email Validation or DNS Validation.
If you choose DNS validation, the stack will spit out a random (secret) DNS Name and DNS Value. If you create that entry correctly, then AWS assumes that you are indeed the owner of that domain and lets you provision the certificate. But you see, a stack itself cannot describe itself in order to know what is the secret entry that should be created, therefore you cannot provision the Certificate on the Serverless Model without a manual process involved.
This is a CFN limitation, so if SAM wants to offer the option to automatically provision the certificate, the user will have to intervene to approve an email or DNS.
For the Custom Domain, we can easily just provision that, the only reason I bring all of this up is that none of this will work (the custom domain will not properly work) if we don't have a certificate.
Receiving an already valid certificate from Parameter or ImportValue is, IMHO, a better option for the user of SAM.

@brettstack
Copy link
Contributor Author

@txase is it not enough for us to just add Condition to all of the generated resources if you specify Condition in the Domain configuration?

@brettstack
Copy link
Contributor Author

@deleugpn

But you see, a stack itself cannot describe itself

I see, the Certificate CFN resource doesn't allow returning the values required to configure the DNS https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-certificatemanager-certificate.html#aws-resource-certificatemanager-certificate-return-values so we can't do it within the same template.

@txase
Copy link
Contributor

txase commented Feb 6, 2019

@brettstack AFAIK, you can't specify a Condition on a property, only on a resource. You could do something like:

Domain: !If
  - CreateCustomDomainCondition
  - !Ref CustomDomainParameter
  - !Ref AWS::NoValue

But how does SAM then turn that into Condition statements on the resources it generates? It could be that when SAM runs as a transform the conditions have already been evaluated and you just have to deal with the property value being either AWS::NoValue or a domain name string. If that's the case, then SAM should be able to do it without too much effort. If not, then things get tricky.

@brettstack
Copy link
Contributor Author

@deleugpn We will definitely allow specifying an existing Certificate via a parameter but I also think the automatic certificate creation workflow is very useful for users, even if it requires them to perform some manual verification step.

@brettstack
Copy link
Contributor Author

@txase You're right, usually you can't specify Condition on CloudFormation Properties, but you can with SAM. Rather, you will soon be able to with SAM. Our next release will include Condition support for multiple Resources/Properties:

#765
#755
#742
#707
#653

@keetonian
Copy link
Contributor

@brettstack Those issues + PRs that you referenced are for conditions on Resources, not individual properties of resources. SAM still does not evaluate conditions, it will just more intelligently pass them on to CFN. I'd be cautious about putting conditions in individual properties; putting conditions on resources is what will soon be supported.

@brettstack
Copy link
Contributor Author

Ahh, I thought #755 added conditions for Events. Either way, I think it's fine adding it to a property like this. This property is just a nested form of a resource; it doesn't modify the properties of the API.

@keetonian
Copy link
Contributor

Sorry, that name is a little misleading. It adds conditions to event resources, not to the event property itself.

@brettstack
Copy link
Contributor Author

Updated proposal to include a simple BasePath option which default to '/' and creates a mapping to this API's Stage (StageName). Specifying BasePathMappings overrides this value (SAM should throw an error if it detects both have been specified).

@onionhammer
Copy link

@danielclariondoor I agree, this is super annoying. Not sure how they can close this without a resolution beyond this hacky workaround

@praneetap
Copy link
Contributor

@deleugpn @danielclariondoor @onionhammer Take a look at this #192 (comment) , the trick is to use !Ref Api.Stage. Hope this helps!

@praneetap
Copy link
Contributor

We are finalizing the design for this feature. Some things to note -

  1. Automatic certificate creation - To enable this on SAM, we would need to create a custom resource or wait for CloudFormation to support this. While we work on getting this prioritized we are going to start implementing this feature with a valid Certificate arn as an input to the template.
  2. The first milestone for this would be to make it easier to create basepath mappings and domain name in api gw. We wont be creating Route53 records in this milestone. [Custom Domains][M1]Implement the base case #1125 [Custom Domains][M1] Implement the Edge endpoint for CustomDomains #1126 [Custom Domains][M1] Implement multiple basepath mappings for single domain single api. #1127
  3. The second milestone would be to add Route 53 support. We cannot create the hosted zone in the same template as certificate validation requires it to be created. So to auto-create Route53 records we will take in hostedzone id as input. The Route53 records type A and AAAA (IpV6) will be autocreated for you for each API gw domain - custom domain mapping. [Custom Domains][M2] Support creating Route53 IpV4 records #1128 [Custom Domains][M2] Support creating Route53 IpV6 records #1129
  4. The third milestone will add support for defining multiple Domains to multiple API mapping. [Custom Domains][M3] Make API property Domain accept a list of objects #1130 Custom Domains: Enable adding basepath mappings in Domain property  #1131

How can you track the progress?
We are trying to make the progress on this more visible by creating a project board.

@scionwest
Copy link

scionwest commented May 18, 2020

This seems limiting though when used in a microservice architecture. I currently have several API Services that are independent SAM deployments - one per service (each service composed of several Lambdas representing API resources).

I am trying to build the Custom Domain as a "core" resource deployed via CloudFormation. I then add BasePathMapping to each SAM template for the individual services I deploy.

Core.yaml

Parameters:
  TargetEnvironment:
    Description: 'Examples are local, dev, test, prod, etc'
    Type: 'String'

Resources:
  ApiDomain:
    Type: AWS::ApiGateway::DomainName
    Properties:
      RegionalCertificateArn: {'Fn::ImportValue': !Sub 'focusmark-${TargetEnvironment}-certificate-api'}
      DomainName: !Sub '${TargetEnvironment}.api.focusmark.app'
      EndpointConfiguration:
        Types:
          - REGIONAL
      SecurityPolicy: TLS_1_2

Outputs:
  ApiCustomDomainName:
    Description: 'Custom domain name for the API'
    Value: !Ref ApiDomain
    Export:
      Name: !Sub 'focusmark-${TargetEnvironment}-apigw-customdomain'
      
  ApiCustomDomainRegionalDomainName:
    Description: 'Regional domain name for the API'
    Value: !GetAtt ApiDomain.RegionalDomainName
    Export:
      Name: !Sub 'focusmark-${TargetEnvironment}-apigw-regionaldomain'

Certificates.yaml

Parameters:
  TargetEnvironment:
    Description: 'Examples are local, dev, test, prod, etc'
    Type: 'String'

Resources:
  CertificateApiDomain:
      Type: AWS::CertificateManager::Certificate
      Properties:
          DomainName: !Sub "${TargetEnvironment}.api.focusmark.app"
          SubjectAlternativeNames:
              - !Sub "${TargetEnvironment}.api.focusmarkapp.com"
          ValidationMethod: DNS
          
Outputs:
  CertificateApiDomain:
    Description: 'ARN of the Certificate created for the api associated to the environment'
    Value: !Ref CertificateApiDomain
    Export:
      Name: !Sub 'focusmark-${TargetEnvironment}-certificate-api'

SAM Service #1: Identity API

Parameters:
  TargetEnvironment:
    Description: 'Examples can be dev, test or prod'
    Type: 'String'

Resources:
  IdentityApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref TargetEnvironment
      Name: !Join [ "-", [ 'focusmark', !Ref TargetEnvironment, 'apigw', 'identity' ] ]
  
  IdentityDomainMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties: 
      BasePath: identity
      DomainName: {'Fn::ImportValue': !Sub 'focusmark-${TargetEnvironment}-apigw-customdomain'}
      RestApiId: !Ref IdentityApi
      Stage: !Ref TargetEnvironment

  IdentityTokenPostLambda:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: 'lambda/'
      FunctionName: !Join [ "-", [ 'focusmark', !Ref TargetEnvironment, 'lambda', 'identity_token_post' ] ]
      Handler: lambda-token-post.handler
      Runtime: nodejs12.x
      Events:
        PostEvent:
          Type: Api
          Properties:
            Path: /oauth2/token
            Method: post
            RestApiId:
              Ref: IdentityApi

SAM Service #2: Chat Service

Parameters:
  TargetEnvironment:
    Description: 'Examples can be dev, test or prod'
    Type: 'String'

Resources:
  ChatApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref TargetEnvironment
      Name: !Join [ "-", [ 'focusmark', !Ref TargetEnvironment, 'apigw', 'chat' ] ]
  
  ChatDomainMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties: 
      BasePath: chat
      DomainName: {'Fn::ImportValue': !Sub 'focusmark-${TargetEnvironment}-apigw-customdomain'}
      RestApiId: !Ref ChatApi
      Stage: !Ref TargetEnvironment

  ChatTokenPostLambda:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: 'lambda/'
      FunctionName: !Join [ "-", [ 'focusmark', !Ref TargetEnvironment, 'lambda', 'chat_post' ] ]
      Handler: lambda-token-post.handler
      Runtime: nodejs12.x
      Events:
        PostEvent:
          Type: Api
          Properties:
            Path: /chat
            Method: post
            RestApiId:
              Ref: ChatApi

This gives me the flexibility to grow my service API foot print organically over time, deploying additional microservices, and not having to go back and touch the Custom Domain itself. I can plug additional services into it. It also allows me to avoid having a single API Gateway for the entire API surface area - just because SAM doesn't support BasePathMappings in a loosely coupled manor.

I know I can achieve this by using AWS::ApiGateway::RestApi coupled with AWS::ApiGateway::BasePathMapping but this breaks sam local start-api. With the example provided above I am not able to actually use it because I can't reference the Stage in the BaseMapMappings resource. There isn't a way for me to reference the Stage resource explicitly that SAM makes without hard-coding the Stage name... In my scenario the Stage name is a parameter and DependsOn won't let me use !Sub to piece together the StageName it should look for.

Please don't couple the only way to handle BasePathMappings and AWS::Serverless by only supporting BasePathMappings on a CustomDomain that is attached to your AWS::Serverless::Api resource - that doesn't scale at all. It also doesn't make any sense for other services to depend on an unrelated service resource in order to access it's CustomDomain that you need to ride under.

@pagpires
Copy link

@scionwest Thanks for your template and I was in the exact same scenario (one existing domain name and gradually increasing API). Have you tried to reference Stage according to this (#313 (comment))? My DependsOn eventually worked with this.

@scionwest
Copy link

@pagpires I tried that originally but it broke the sam local start-api CLI command that I depend on heavily to test within Cloud9. Maybe it's fixed and works now, I haven't tried it recently.

@emaayan
Copy link

emaayan commented Dec 2, 2020

how can you reference the regionalDomainName from the generated resource domainName if that name is coupled with generated sha

@twasink
Copy link

twasink commented Feb 24, 2021

I've got the same use case as @scionwest - I want to define a custom domain name (api.mysite.url), and deploy multiple APIs to it (e.g. https://api.mysite.url/apimodule1/, https://api.mysite.url/apimodule2/). These extra APIs would be defined in different SAM templates.

This can be managed with AWS::ApiGatewayV2 resources, but doing it via SAM seems problematic, because I can't provide a pre-defined AWS::ApiGatewayV2::DomainName to a AWS::Serverless::HttpApi or AWS::Serverless::Api.

I can't even define an explicit AWS::ApiGatewayV2::ApiMapping because the stage isn't exported from the SAM Api/HttpApi objects

@jfuss
Copy link
Contributor

jfuss commented Aug 9, 2021

Most of the work has been completed (for some time) on Custom Domains. It seems like there is one outstanding issue #1131, could be others I missed.

Given Custom Domains has been shipped, I am going to close this. If there are missing features or bugs outstanding, please create a new issue (if one hasn't been created yet). This helps us keep track of work without having to read long threads and extract different issues/requests after the fact. We do look at Github Reactions to help us understand what the community is asking for, so please add reactions to any you feel are important.

@jfuss jfuss closed this as completed Aug 9, 2021
@babaMar
Copy link

babaMar commented Mar 9, 2022

I imagine most users want to have a single domain mapped to a single API. If that's the case, then leaving it as a single Domain is fine. However, if a significant number of users are likely to map multiple Domains to a single API+Stage, we should consider allowing a list of Domains. We can always launch initially with a single Domain and modify Domain to also accept an Array in the future. We just need to think this through to make sure we don't go through any one-way doors that would be backwards incompatible.

It seems this is still not supported, correct?

@scionwest
Copy link

scionwest commented Mar 20, 2022

@twasink & @babaMar I've been able to get around these issues using the following:

First I create the certificate that I need to use for my APIs custom domain. This assumes you have a Route 53 Hosted Zone exported in Cloud Formation for importing. If I'm running in prod then the cert is for https://api.productname.com. Otherwise it is prefixed with the environment like https://api-dev.productname.com

certificates.yaml

Parameters:
  TargetEnvironment:
    Description: 'Examples are local, dev, test, prod, etc'
    Type: 'String'
    
  ProductName:
    Description: 'Represents the name of the product you want to call the deployment'
    Type: 'String'

Conditions: 
  CreateProdResources: !Equals [ !Ref TargetEnvironment, prod ]
  
Resources:
  CertificateApiDomain:
      Type: AWS::CertificateManager::Certificate
      Properties:
          DomainName: !If [CreateProdResources, !Sub "api.${ProductName}.app", !Sub "${TargetEnvironment}-api.${ProductName}.app"]
          DomainValidationOptions:
            - DomainName:  !If [CreateProdResources, !Sub "api.${ProductName}.app", !Sub "${TargetEnvironment}-api.${ProductName}.app"]
              HostedZoneId:  {'Fn::ImportValue': !Sub '${ProductName}-route53-dotAppZone'}
          ValidationMethod: DNS
          #SubjectAlternativeNames:
          #    - !If [CreateProdResources, !Sub "api.${ProductName}app.com", !Sub "${TargetEnvironment}-api.${ProductName}app.com"]
          
Outputs:
  CertificateApiDomain:
    Description: 'ARN of the Certificate created for the api associated to the environment'
    Value: !Ref CertificateApiDomain
    Export:
      Name: !Sub '${ProductName}-${TargetEnvironment}-certificate-api'

Then I create the API Gateway custom domain with my cert and specify the endpoint as Regional.

api-gateway-domain

Parameters:
  TargetEnvironment:
    Description: 'Examples are local, dev, test, prod, etc'
    Type: 'String'
    
  ProductName:
    Description: 'Represents the name of the product you want to call the deployment'
    Type: 'String'
    
Resources:
  
  ApiDomain:
    Type: AWS::ApiGateway::DomainName
    Properties:
      RegionalCertificateArn: {'Fn::ImportValue': !Sub '${ProductName}-${TargetEnvironment}-certificate-api'}
      DomainName: !Sub '${TargetEnvironment}-api.${ProductName}.app'
      EndpointConfiguration:
        Types:
          - REGIONAL
      SecurityPolicy: TLS_1_2
          
Outputs:
  ApiCustomDomainName:
    Description: 'Custom domain name for the API'
    Value: !Ref ApiDomain
    Export:
      Name: !Sub '${ProductName}-${TargetEnvironment}-apigw-customdomain'
      
  ApiCustomDomainRegionalDomainName:
    Description: 'Regional domain name for the API'
    Value: !GetAtt ApiDomain.RegionalDomainName
    Export:
      Name: !Sub '${ProductName}-${TargetEnvironment}-apigw-regionaldomain'
      
  ApiCustomDomainRegionalDomainHostedZone:
    Description: 'Regional domain hosted zone for the API'
    Value: !GetAtt ApiDomain.RegionalHostedZoneId
    Export:
      Name: !Sub '${ProductName}-${TargetEnvironment}-apigw-regionalhostedzoneid'

Next I deploy a SAM template containing an API Gateway and a Lambda. This API Gateway will only ever be used by this Lambda. If I need more Lambdas for different micro-services (such as a Task API to go with this Project API) then I would deploy a whole new SAM template with a dedicated Lambda and API Gateway.

project-api-sam.yaml

AWSTemplateFormatVersion: 2010-09-09
Transform:
- AWS::Serverless-2016-10-31

Parameters:
  TargetEnvironment:
    Description: 'Examples can be dev, test or prod'
    Type: 'String'    
  ProductName:
    Description: 'Represents the name of the product you want to call the deployment'
    Type: 'String'

Resources:
  ProjectApi:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref TargetEnvironment
      Name: !Sub '${ProductName}-${TargetEnvironment}-apigateway-project'
              
  getAllItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      Handler: src/handlers/get-all-handler.getAllHandler
      FunctionName: !Sub '${ProductName}-${TargetEnvironment}-lambda-api_project_getall'
      Runtime: nodejs12.x
      MemorySize: 256
      Role: !GetAtt getAllItemsFunctionRole.Arn
      Events:
        Api:
          Type: Api
          Properties:
            Path: /
            Method: GET
            RestApiId:
              Ref: ProjectApi

Outputs:
  ProjectApiEndPoint:
    Description: "API Gateway endpoint URL for target environment stage"
    Value: !Sub "https://${ProjectApi}.execute-api.${AWS::Region}.amazonaws.com/${TargetEnvironment}/"
  ProjectApiId:
    Description: "ID of the Project API resource"
    Value: !Ref ProjectApi
    Export:
      Name: !Sub '${ProductName}-${TargetEnvironment}-apigateway-project'

Finally I marry the API Gateways together using a dedicated CloudFormation template that maps the base mappings. Now each Microservice can have it's own API Gateway and series of Lambdas while I use a single API domain.

project-api-gateway.yaml

Parameters:
  TargetEnvironment:
    Description: 'Examples can be dev, test or prod'
    Type: 'String'
    
  ProductName:
    Description: 'Represents the name of the product you want to call the deployment'
    Type: 'String'
    
Resources:
  ProjectApiDomainMapping:
    Type: AWS::ApiGateway::BasePathMapping
    Properties: 
      BasePath: project
      DomainName: {'Fn::ImportValue': !Sub '${ProductName}-${TargetEnvironment}-apigw-customdomain'}
      RestApiId: {'Fn::ImportValue': !Sub '${ProductName}-${TargetEnvironment}-apigateway-project'}
      Stage: !Ref TargetEnvironment

With this I am able to use sam local start-api. I've been doing this for a while now so I'm not sure if it's just the way my CF is now structured that allowed for this or if SAM made changes to improve the support. This works great those for microservice platforms and running local development.

I have a complete example if you'd like to browse the repository: https://github.com/focusmark/home. It links to each of the repositories for core infrastructure (logging/iam), auth (cognito/oauth), project & tasks (micro-service APIs) and DNS (for mapping API Gateways to BasePathMappings). If you use GoDaddy as your Domain provider there is also a custom resource for assigning your GoDaddy nameservers to Route 53 so you can manage your DNS in AWS.

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

No branches or pull requests