serverless-application-model icon indicating copy to clipboard operation
serverless-application-model copied to clipboard

When defining two API's with domains in the same hosted zone, the second API's Route 53 RecordSetGroups resource overwrites the first

Open NickDobsonMO opened this issue 5 years ago • 9 comments

Description:

When you define two AWS::Serverless::Api resources in the same template which have domains in the same Route 53 hosted zone the resulting Cloudformation template contains one AWS::Route53::RecordSetGroup which only includes the record set for the second API. The recordset for the first API is not present in the output and is lost.

During the transform, the logical id for the AWS::Route53::RecordSetGroup is generated using a hash of either the hosted zone name or hosted zone id. As the same hosted zone is used for both AWS::Serverless::Api resources domains, the logic name is the same and the AWS::Route53::RecordSetGroup created for the second AWS::Serverless::Api domain overwrites the first.

Steps to reproduce the issue:

  1. Create a template containing two gateways with domains in the same hosted zone:
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31

Resources:
    ApiGatewayOne:
        Type: AWS::Serverless::Api
        Properties:
            StageName: Prod
            Domain:
                CertificateArn: CertificateArn
                DomainName: api-one.domain.com
                Route53:
                    HostedZoneName: HostedZoneName

    ApiGatewayTwo:
        Type: AWS::Serverless::Api
        Properties:
            StageName: Prod
            Domain:
                CertificateArn: CertificateArn
                DomainName: api-two.domain.com
                Route53:
                    HostedZoneName: HostedZoneName
  1. Transform the SAM template to Cloudformation using the CLI or console.

Observed result:

AWSTemplateFormatVersion: '2010-09-09'
Resources:
  ApiGatewayOne:
    Properties:
      Body:
        info:
          title:
            Ref: AWS::StackName
          version: '1.0'
        paths: {}
        swagger: '2.0'
    Type: AWS::ApiGateway::RestApi
  ApiGatewayOneDeploymentf9e2e3fce7:
    Properties:
      Description: 'RestApi deployment id: f9e2e3fce7ff39be96068344e44c14271beacd7e'
      RestApiId:
        Ref: ApiGatewayOne
      StageName: Stage
    Type: AWS::ApiGateway::Deployment
  ApiGatewayOneProdStage:
    Properties:
      DeploymentId:
        Ref: ApiGatewayOneDeploymentf9e2e3fce7
      RestApiId:
        Ref: ApiGatewayOne
      StageName: Prod
    Type: AWS::ApiGateway::Stage
  ApiGatewayDomainNamea053849895:
    Properties:
      DomainName: api-one.domain.com
      EndpointConfiguration:
        Types:
          - REGIONAL
      RegionalCertificateArn: CertificateArn
    Type: AWS::ApiGateway::DomainName
  ApiGatewayOneBasePathMapping:
    Properties:
      DomainName:
        Ref: ApiGatewayDomainNamea053849895
      RestApiId:
        Ref: ApiGatewayOne
      Stage:
        Ref: ApiGatewayOneProdStage
    Type: AWS::ApiGateway::BasePathMapping
  RecordSetGroupf6dbc85119:  # This RecordSetGroup only contains the second APIs recordsets
    Properties:
      HostedZoneName: HostedZoneName
      RecordSets:
        - AliasTarget:
            DNSName:
              Fn::GetAtt:
                - ApiGatewayDomainName9efebe563e
                - RegionalDomainName
            HostedZoneId:
              Fn::GetAtt:
                - ApiGatewayDomainName9efebe563e
                - RegionalHostedZoneId
          Name: api-two.domain.com
          Type: A
    Type: AWS::Route53::RecordSetGroup
  ApiGatewayTwo:
    Properties:
      Body:
        info:
          title:
            Ref: AWS::StackName
          version: '1.0'
        paths: {}
        swagger: '2.0'
    Type: AWS::ApiGateway::RestApi
  ApiGatewayTwoDeploymenta139d06cd0:
    Properties:
      Description: 'RestApi deployment id: a139d06cd0ba5773c1d94d9d67cd7eaf0b25121f'
      RestApiId:
        Ref: ApiGatewayTwo
      StageName: Stage
    Type: AWS::ApiGateway::Deployment
  ApiGatewayTwoProdStage:
    Properties:
      DeploymentId:
        Ref: ApiGatewayTwoDeploymenta139d06cd0
      RestApiId:
        Ref: ApiGatewayTwo
      StageName: Prod
    Type: AWS::ApiGateway::Stage
  ApiGatewayDomainName9efebe563e:
    Properties:
      DomainName: api-two.domain.com
      EndpointConfiguration:
        Types:
          - REGIONAL
      RegionalCertificateArn: CertificateArn
    Type: AWS::ApiGateway::DomainName
  ApiGatewayTwoBasePathMapping:
    Properties:
      DomainName:
        Ref: ApiGatewayDomainName9efebe563e
      RestApiId:
        Ref: ApiGatewayTwo
      Stage:
        Ref: ApiGatewayTwoProdStage
    Type: AWS::ApiGateway::BasePathMapping

Expected result:

Either two AWS::Route53::RecordSetGroup resources, one for each AWS::Serverless::Api domain. Or one AWS::Route53::RecordSetGroup containing the record sets for both AWS::Serverless::Api domains.

NickDobsonMO avatar Mar 26 '20 14:03 NickDobsonMO

Any known work arounds in the meantime?

karuhanga avatar Jun 08 '20 15:06 karuhanga

@keetonian There is a workaround, but it only works if the template contains a maximum of two AWS::Serverless::Api resources with Route53 domains in the same template.

Within the AWS::Serverless::Api - DomainConfiguration - Route53Configuration properties, the first Api can use HostedZoneName while the second uses HostedZoneId, this results in the transformed template containing two AWS::Route53::RecordSetGroup resources, one for each AWS::Serverless::Api.

This work around doesn't work if the template contains more than two AWS::Serverless::Api resources with Route53 domains, as either the HostedZoneName or HostedZoneId are used to generate the AWS::Route53::RecordSetGroup logical ID and once both properties have been used, subsequent logical IDs will clash and overwrite the first in the transformed template.

NickDobsonMO avatar Jun 09 '20 07:06 NickDobsonMO

Relevant code seems to be at https://github.com/awslabs/serverless-application-model/blob/11a20ae31c9f81bf5ccdc5f04b01aa262cbacca5/samtranslator/model/api/api_generator.py#L346

ilyash-b avatar Jul 02 '20 11:07 ilyash-b

I have a "consumer API" and an "admin API" in my SAM so thankfully @NickDobsonMO's work around worked for me. I did get hung up with the fact that HostedZoneName requires the trailing . even though it isn't displayed in the Route53 console.

~If/when this gets fixed, please be aware that the workaround is actively used.~

I don't think I can recommend this workaround anymore. It seems very incidental that it works and as soon as I tried to configured the BasePath value, it once again breaks down.

Only one base path mapping is allowed if the base path is empty. (Service: AmazonApiGateway; Status Code: 409; Error Code: ConflictException; Request ID: 12341234-cc00-4779-941b-dbfd035cdf1a; Proxy: null) 

I'm throwing in the towel with this and breaking my application into separate SAM templates. I end up duplicating code but it feels safer.

mousedownmike avatar Nov 08 '20 23:11 mousedownmike

Clearly this issue is caused by using the HostedZoneId and HostedZoneName as the source for generating a resource Logical ID.

If we can agree this is the root-cause, I would propose that rest_api.logical_id be passed to LogicalIdGenerator for the RecordSetGroup. This will result in a unique RecordSetGroup for each AWS::Serverless::Api. Obviously, this is only one possible solution to resolve the symptom of having duplicate Logical IDs with the last overwriting all subsequent.

travisdieckmann avatar Dec 18 '20 14:12 travisdieckmann

@hardweezy Thanks for your comment. The issue is specifically when using the same HostedZoneId or when using the same HostedZoneName within the same template for multiple APIs.

travisdieckmann avatar Mar 26 '21 13:03 travisdieckmann

Improving on the workaround, my example works with a maximum of three AWS::Serverless::Api resources with Route53 domains in the same template.

I was able to provide two different 'HostedZoneId' properties and one 'HostedZoneName' property in the Route53 configurations in each API in my template.

    Route53:
      DistributionDomainName: test.domain.name
      HostedZoneId: Z0431057216U6FBZHHDJK8

    Route53:
      DistributionDomainName: main.domain.name
      HostedZoneId: /hostedzone/Z0431057216U6FBZHHDJK8

    Route53:
      DistributionDomainName: main2.domain.name
      HostedZoneName: domain.name.

As you can see the above 'HostedZoneId' properties are different enough to generate a different hash each, which allowed two 'AWS::Route53::RecordSetGroup' resources to be created. I have tested using all 3 above Route53 Configurations which means you can have up to three Serverless APIs using Route53 Configuration in one template.

hardweezy avatar Mar 26 '21 14:03 hardweezy

Thanks @NickDobsonMO and @hardweezy for providing the walkarounds you saved my day!

jacqueskang avatar Jun 01 '21 17:06 jacqueskang

Has an issue been created for this? I see workarounds but think this should actually be solved at some point as well?

nickswiss avatar Feb 21 '22 19:02 nickswiss

This issue has been fixed by https://github.com/aws/serverless-application-model/pull/2408.

Closing this issue now. Feel free to reopen it if there are any questions.

xazhao avatar Dec 07 '22 00:12 xazhao