How do I configure my PUBLIC AWS custom domain to resolve to a lambda that is configured for my VPC?

For Reference:

AWS-GENERATED-URL:https://xxx.execute-api.us-east-1.amazonaws.com/prod/outages

MY-CUSTOM-DOMAIN-URL: https://api.foo.com/outages

What is working:

Using the serverless framework:

  • I have configured an AWS VPC
  • I have an Amazon Aurora database configured for my VPC
  • I have an AWS API Gateway lambda that is configured for my VPC
  • When I deploy my lambda, I am able to access it publicly via the AWS-GENERATED-URL
  • In my Lambda,I run a very simple query that proves I can connect to my database.

This all works fine.

What is NOT working:

  • I have registered a domain with AWS/Route 53 and added a cert (e.g. *.foo.com)

  • I use the serverless-domain-manager plugin to make my lambda available via my domain (e.g. MY-CUSTOM-DOMAIN-URL resolves to the AWS-GENERATED-URL)

This works fine if my lambda is NOT configured for my VPC

But when my lambda IS configured for my VPC, the custom domain MY-CUSTOM-DOMAIN-URL does NOT resolve to the AWS-GENERATED-URL.

I other words: I can NOT access MY-CUSTOM-DOMAIN-URL publicly.

What I need is:

1 - The AWS-GENERATED-URL is available publicly (this works)

2 - My custom domain, MY-CUSTOM-DOMAIN-URL points to the SAME lambda as the AWS-GENERATED-URL (in my VPC) and is available publicly (not working. I get: {“message”:“Forbidden”})

virtual-private-cloud.yml

service: virtual-private-cloud

provider:
  name: aws
  region: us-east-1
  stage: ${opt:stage, dev}

custom:
  appVersion: 0.0.0
  VPC_CIDR: 10

resources:
  Resources:
    ServerlessVPC:
      Type: AWS::EC2::VPC
      Properties:
        CidrBlock: ${self:custom.VPC_CIDR}.0.0.0/16
        EnableDnsSupport: true
        EnableDnsHostnames: true
        InstanceTenancy: default
    ServerlessSubnetA:
      DependsOn: ServerlessVPC
      Type: AWS::EC2::Subnet
      Properties:
        VpcId:
          Ref: ServerlessVPC
        AvailabilityZone: ${self:provider.region}a
        CidrBlock: ${self:custom.VPC_CIDR}.0.0.0/24
    ServerlessSubnetB:
      DependsOn: ServerlessVPC
      Type: AWS::EC2::Subnet
      Properties:
        VpcId:
          Ref: ServerlessVPC
        AvailabilityZone: ${self:provider.region}b
        CidrBlock: ${self:custom.VPC_CIDR}.0.1.0/24
    ServerlessSubnetC:
      DependsOn: ServerlessVPC
      Type: AWS::EC2::Subnet
      Properties:
        VpcId:
          Ref: ServerlessVPC
        AvailabilityZone: ${self:provider.region}c
        CidrBlock: ${self:custom.VPC_CIDR}.0.2.0/24

  Outputs:
    VPCDefaultSecurityGroup:
      Value:
        Fn::GetAtt:
          - ServerlessVPC
          - DefaultSecurityGroup
      Export:
        Name: VPCDefaultSecurityGroup-${self:provider.stage}
    SubnetA:
      Description: 'Subnet A.'
      Value: !Ref ServerlessSubnetA
      Export:
        Name: vpc-subnet-A-${self:provider.stage}
    SubnetB:
      Description: 'Subnet B.'
      Value: !Ref ServerlessSubnetB
      Export:
        Name: vpc-subnet-B-${self:provider.stage}
    SubnetC:
      Description: 'Subnet C.'
      Value: !Ref ServerlessSubnetC
      Export:
        Name: vpc-subnet-C-${self:provider.stage} 

database-service.yml

service: database-service

provider:
  name: aws
  region: us-east-1
  stage: ${opt:stage, dev}
  environment:
    stage: ${opt:stage, dev}

plugins:
  - serverless-plugin-ifelse

custom:
  appVersion: 0.0.1
  AURORA:
    DB_NAME: database${self:provider.stage}
    USERNAME: ${ssm:/my-db-username~true}
    PASSWORD: ${ssm:/my-db-password~true}
    HOST:
      Fn::GetAtt: [AuroraRDSCluster, Endpoint.Address]
    PORT:
      Fn::GetAtt: [AuroraRDSCluster, Endpoint.Port]
  serverlessIfElse:
    - If: '"${opt:stage}" == "prod"'
      Set:
        resources.Resources.AuroraRDSCluster.Properties.EngineMode: provisioned
      ElseSet:
        resources.Resources.AuroraRDSCluster.Properties.EngineMode: serverless
        resources.Resources.AuroraRDSCluster.Properties.ScalingConfiguration.MinCapacity: 1
        resources.Resources.AuroraRDSCluster.Properties.ScalingConfiguration.MaxCapacity: 4

      ElseExclude:
        - resources.Resources.AuroraRDSInstanceParameter
        - resources.Resources.AuroraRDSInstance

resources:
  Resources:
    AuroraSubnetGroup:
      Type: AWS::RDS::DBSubnetGroup
      Properties:
        DBSubnetGroupDescription: "Aurora Subnet Group"
        SubnetIds:
          - 'Fn::ImportValue': vpc-subnet-A-${self:provider.stage}
          - 'Fn::ImportValue': vpc-subnet-B-${self:provider.stage}
          - 'Fn::ImportValue': vpc-subnet-C-${self:provider.stage}
    AuroraRDSClusterParameter:
      Type: AWS::RDS::DBClusterParameterGroup
      Properties:
        Description: Parameter group for the Serverless Aurora RDS DB.
        Family: aurora5.6
        Parameters:
          character_set_database: "utf32"
    AuroraRDSCluster:
      Type: "AWS::RDS::DBCluster"
      Properties:
        MasterUsername: ${self:custom.AURORA.USERNAME}
        MasterUserPassword: ${self:custom.AURORA.PASSWORD}
        DBSubnetGroupName:
          Ref: AuroraSubnetGroup
        Engine: aurora
        EngineVersion: "5.6.10a"
        DatabaseName: ${self:custom.AURORA.DB_NAME}
        BackupRetentionPeriod: 3
        DBClusterParameterGroupName:
          Ref: AuroraRDSClusterParameter
        VpcSecurityGroupIds:
          - 'Fn::ImportValue': VPCDefaultSecurityGroup-${self:provider.stage}

    # this is needed for non-serverless mode
    AuroraRDSInstanceParameter:
      Type: AWS::RDS::DBParameterGroup
      Properties:
        Description: Parameter group for the Serverless Aurora RDS DB.
        Family: aurora5.6
        Parameters:
          sql_mode: IGNORE_SPACE
          max_connections: 100
          wait_timeout: 900
          interactive_timeout: 900

    # this is needed for non-serverless mode
    AuroraRDSInstance:
      Type: "AWS::RDS::DBInstance"
      Properties:
        DBInstanceClass: db.t2.small
        DBSubnetGroupName:
          Ref: AuroraSubnetGroup
        Engine: aurora
        EngineVersion: "5.6.10a"
        PubliclyAccessible: false
        DBParameterGroupName:
          Ref: AuroraRDSInstanceParameter
        DBClusterIdentifier:
          Ref: AuroraRDSCluster

  Outputs:
    DatabaseName:
      Description: 'Database name.'
      Value: ${self:custom.AURORA.DB_NAME}
      Export:
        Name: DatabaseName-${self:provider.stage}
    DatabaseHost:
      Description: 'Database host.'
      Value: ${self:custom.AURORA.HOST}
      Export:
        Name: DatabaseHost-${self:provider.stage}
    DatabasePort:
      Description: 'Database port.'
      Value: ${self:custom.AURORA.PORT}
      Export:
        Name: DatabasePort-${self:provider.stage}

outage-service.yml

service: outage-service

package:
  individually: true

plugins:
  - serverless-bundle
  - serverless-plugin-ifelse
  - serverless-domain-manager

custom:
  appVersion: 0.0.12
  stage: ${opt:stage}
  domains:
    prod: api.foo.com
    test: test-api.foo.com
    dev: dev-api.foo.com
  customDomain:
    domainName: ${self:custom.domains.${opt:stage}}
    stage: ${opt:stage}
    basePath: outages
    custom.customDomain.certificateName: "*.foo.com"
    custom.customDomain.certificateArn: 'arn:aws:acm:us-east-1:395671985612:certificate/XXXX'
    createRoute53Record: true
  serverlessIfElse:
    - If: '"${opt:stage}" == "prod"'
      Set:
          custom.customDomain.enabled: true
      ElseSet:
          custom.customDomain.enabled: false

provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage}
  region: us-east-1
  environment:
    databaseName: !ImportValue DatabaseName-${self:provider.stage}
    databaseUsername: ${ssm:/my-db-username~true}
    databasePassword: ${ssm:/my-db-password~true}
    databaseHost: !ImportValue DatabaseHost-${self:provider.stage}
    databasePort: !ImportValue DatabasePort-${self:provider.stage}

functions:
  hello:
    memorySize: 2048
    timeout: 30
    handler: functions/handler.hello
    vpc:
      securityGroupIds:
        - 'Fn::ImportValue': VPCDefaultSecurityGroup-${self:provider.stage}
      subnetIds:
        - 'Fn::ImportValue': vpc-subnet-A-${self:provider.stage}
        - 'Fn::ImportValue': vpc-subnet-B-${self:provider.stage}
        - 'Fn::ImportValue': vpc-subnet-C-${self:provider.stage}
    environment:
      functionName: getTowns
    events:
      - http:
          path: outage
          method: get
          cors:
            origin: '*'
            headers:
              - Content-Type
              - authorization

resources:
  - Outputs:
      ApiGatewayRestApiId:
        Value:
          Ref: ApiGatewayRestApi
        Export:
          Name: ${self:custom.stage}-ApiGatewayRestApiId
    
      ApiGatewayRestApiRootResourceId:
        Value:
           Fn::GetAtt:
            - ApiGatewayRestApi
            - RootResourceId 
        Export:
          Name: ${self:custom.stage}-ApiGatewayRestApiRootResourceId