CloudFormation fails with Route53 RecordSet and ApiGatewayV2 Domain - amazon-web-services

I am using Serverless to create a WebSocket API with AWS. I have added the following additional CloudFormation resources inside the serverless.yml file:
Resources:
ApiGatewayDomain:
Type: AWS::ApiGatewayV2::DomainName
Properties:
DomainName: ${opt:id}.example.com
DomainNameConfigurations:
- EndpointType: REGIONAL
CertificateArn: arn:aws:acm:eu-central-1:<REDACTED>:certificate/<REDACTED>
ApiGatewayBasePathMapping:
Type: AWS::ApiGatewayV2::ApiMapping
Properties:
DomainName: # ${opt:id}.example.com
Ref: ApiGatewayDomain
ApiId:
Ref: "WebsocketsApi"
Stage: ${opt:stage, 'dev'}
DependsOn:
- WebsocketsDeployment${sls:instanceId}
- WebsocketsDeploymentStage
Route53RecordSet:
Type: AWS::Route53::RecordSet
Properties:
Type: A
HostedZoneId: <REDACTED>
Name:
Ref: ApiGatewayDomain
AliasTarget:
DNSName: # <REDACTED>.execute-api.eu-central-1.amazonaws.com.
Fn::Join:
- ""
-
- Fn::GetAtt: [ApiGatewayDomain, RegionalDomainName]
- "."
HostedZoneId: # Z1U9ULNL0V5AJ3
Fn::GetAtt: [ApiGatewayDomain, RegionalHostedZoneId]
DependsOn:
- ApiGatewayDomain
I want to achieve, that the deployment of a stack registers a custom api gateway domain (works), sets up the path mapping (works) and adds a Route53RecordSet (only works with concrete values).
I cannot figure out what I am doing wrong and always get an internal error from Route53. If I comment out the Fn::Join and Fn::GetAtt blocks for DNSName and HostedZoneId and use concrete values, everything works as expected. Am I missing something or is this simply an error with CloudFormation and Route53?

Related

How to link custom domain name to multi region api gateway?

I have created a hosted zone resource via cloudformation. Now i want to create an api gateway via same template in two regions, say us-east-1 and us-west-1 .
customDomain:
Type: AWS::Route53::HostedZone
Properties:
Name: 'example.com'
I understand , i will have to create ssl certificate in both regions. so i have created another stack below. but how do i link , my example.com to api gateway on both the regions.
the error i'm getting now , if i deploy the stack below to second region is - create failed for Mycustomdomainconfig => "The domain name you provided already exists. (service: ApiGateway, ...Status Code 400...
Resources:
MyCertificate:
Type: "AWS::CertificateManager::Certificate"
Properties:
DomainName: example.com
ValidationMethod: DNS
DomainValidationOptions:
- DomainName: 'example.com'
HostedZoneId: 'abcde'
Myrecord:
Type: AWS::Route53::RecordSet
Properties:
Region: !Ref aws_region
SetIdentifier: !Sub "endpoint-${aws_region}"
HostedZoneId: 'abcde'
Name: 'example.com'
Type: A
AliasTarget:
DNSname: !GetAtt Mycustomdomainconfig.RegionalDomainName
HostedZoneId: !GetAtt Mycustomdomainconfig.RegionalHostedZoneId
Mycustomdomainconfig:
Type: AWS::ApiGateway::DomainName
Properties:
RegionalCertificationArn: !Ref MyCertificate
DomainName: 'example.com'
...
MyApiMapping:
Type: AWS::ApiGateway::BasePathMapping
Properties:
DomainName: !Ref Mycustomdomainconfig
RestApiId: !Ref MyApi
...
MyApi:
Type: AWS::Serverless::Api
Properties:
....

BaseUrl Mapping in AWS with inconsistencies

I am creating custom domain for api gateway using yaml. Here is my sample yaml.
TestAppApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref ApiStageName
EndpointConfiguration: REGIONAL
TracingEnabled: TRUE
MethodSettings:
- LoggingLevel: INFO
ResourcePath: "/*" # allows for logging on any resource
HttpMethod: "*" # allows for logging on any method
DataTraceEnabled: TRUE
DomainName:
Type: AWS::ApiGateway::DomainName
Properties:
RegionalCertificateArn: !Ref Certificate
DomainName: !Ref ApiDomain
SecurityPolicy: TLS_1_2
EndpointConfiguration:
Types:
- REGIONAL
UrlMapping:
Type: AWS::ApiGateway::BasePathMapping
DependsOn:
- TestAppApiStage
- DomainName
Properties:
DomainName:
!Ref ApiDomain
RestApiId:
Ref: TestAppApi
Stage: !Ref ApiStageName
Domain:
Type: AWS::Route53::RecordSetGroup
DependsOn:
- TestAppApi
- UrlMapping
- DomainName
Properties:
HostedZoneId: !Ref ApiDomainHostedZoneId
RecordSets:
- Name: !Ref ApiDomain
Type: A
AliasTarget:
DNSName: !GetAtt DomainName.RegionalDomainName
HostedZoneId: !GetAtt DomainName.RegionalHostedZoneId
I am passing ApiDomain as parameter and it works as expected when my ApiDomain is 'dev.test.com.au' where test.com.au is my registered domain. But if I change my ApiDomain to something like 'dev-$env:USERNAME.test.com.au' and then I get the following error.
Invalid domain name identifier specified(Service:AmazonApiGateway;Status Code: 404;
Error Code: NotFoundException;Request ID: fbf8ce40-cce0-4a0f-9714-c649d35a3ee4; Proxy: null)
Apparently this error has something to do with the dependency issue according to this thread https://github.com/aws/serverless-application-model/issues/192# but it works with one name but not the other. And anyways I have added all the dependencies and still invalid domain name identifier error.
Any help will be appreciated!
Don't have capital letters in your domain name people. My ApiDomain parameter has capital letters. Domain name creation automatically converts it to lower letters. But baseurlmapping is left to find ApiDomain with capital letter and hence the issue. Needed to read documentation more clearly but anyhow posting it here in case somebody experiences this silly issue.

How to set up Custom Domain names with Route53 in AWS SAM Cloud-Formation

ORIGINAL QUESTIONS: How to get RegionalDomainName out of a AWS APIGateway DomainName in SAM Cloud-formation
EDIT: I changed the question to hopefully get more traffic to this answer as it answers several questions not just my original one.
I am getting the following error when I try and deploy my stack:
resource DomainName does not support attribute type regionalDomainName in Fn::GetAtt
My yml file looks like the following:
PublicApi:
Type: AWS::Serverless::Api
Properties:
Name: PublicApi
...
EndpointConfiguration: REGIONAL
DomainName:
Type: AWS::ApiGateway::DomainName
Properties:
RegionalCertificateArn: "arn:aws:acm:${Region}:XXXXXXXXXXX:certificate/XXXXXXXXXXXXX"
DomainName: !Sub ${Stage}.${name}
EndpointConfiguration:
Types:
- REGIONAL
myDNSRecord:
Type: AWS::Route53::RecordSet
Properties:
HostedZoneId : Z1UJRXOUMOOFQ8
Name: !Sub ${Stage}.${name}
AliasTarget:
HostedZoneId: Z1UJRXOUMOOFQ8
DNSName: !GetAtt DomainName.regionalDomainName
Type: A
UrlMapping:
Type: AWS::ApiGateway::BasePathMapping
DependsOn:
- PublicApi
Properties:
DomainName: !Ref DomainName
RestApiId: !Ref PublicApi
Stage: !Ref Stage
The following is from the DomainName documentation:
"RegionalDomainName
The domain name associated with the regional endpoint for this custom domain name. You set up this association by adding a DNS record that points the custom domain name to this regional domain name."
I am confused as to what I need to do to make this DomainName Regional.
I have also been getting the following Errors in different iterations, that I thought this should fix:
"Cannot import certificates for EDGE while REGIONAL is active."
Any help on this front would be much appreciated!
I found from several different forums different solutions to different problems and finally came up with a working model. I hope this helps someone out in the future.
PublicApi:
Type: AWS::Serverless::Api
Properties:
Name: PublicApi
StageName: ApiStage
...
EndpointConfiguration: REGIONAL
DomainName:
Type: AWS::ApiGateway::DomainName
Properties:
RegionalCertificateArn: "arn:aws:acm:u${Region}:XXXXXXXX:certificate/XXXXXXXX"
DomainName: stage.example.com
SecurityPolicy: TLS_1_2
EndpointConfiguration:
Types:
- REGIONAL
LambdaDNS:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName:
Ref: example.com.
RecordSets:
- Name:
Fn::Sub: stage.example.com.
Type: A
AliasTarget:
HostedZoneId: Z1UJRXOUMOOFQ8
DNSName:
Fn::GetAtt:
- DomainName
- RegionalDomainName
UrlMapping:
Type: AWS::ApiGateway::BasePathMapping
DependsOn:
- PublicApi
- PublicApiStage
Properties:
DomainName:
Ref: DomainName
RestApiId:
Ref: PublicApi
Stage: ApiStage
The key bit for me ended up being the DependsOn in the BasePathMapping.

Creating a CNAME AWS RecordSet/Custom Domain connection to an API in SAM

I am trying to use Route53 and Custom Domains to link a URL I have and the API gateway that is a Lambda Proxy underneath it.
This is what I have so far in my SAM file:
MyApiCertificate:
Type: 'AWS::CertificateManager::Certificate'
Properties:
DomainName: example.com
MyApiDomainName:
Type: 'AWS::ApiGateway::DomainName'
Properties:
RegionalCertificateArn: !Ref MyApiCertificate
DomainName: example.com
MyApiBasePathMapping:
Type: 'AWS::ApiGateway::BasePathMapping'
Properties:
RestApiId: !Ref PublicApi
DomainName: !Ref MyApiDomainName
BasePath: /
Stage: stage
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
I would like to make this of type CNAME and have the URL be stage.example.com
The following Code does not work.
MyApiCertificate:
Type: 'AWS::CertificateManager::Certificate'
Properties:
DomainName: example.com
MyApiDomainName:
Type: 'AWS::ApiGateway::DomainName'
Properties:
RegionalCertificateArn: !Ref MyApiCertificate
DomainName: example.com
MyApiBasePathMapping:
Type: 'AWS::ApiGateway::BasePathMapping'
Properties:
RestApiId: !Ref PublicApi
DomainName: !Ref MyApiDomainName
BasePath: /
Stage: stage
MyApiRoute53RecordSetGroup:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: example.com
RecordSets:
- Name: stage.example.com
Type: CNAME
AliasTarget:
EvaluateTargetHealth: false
HostedZoneId: !GetAtt MyApiDomainName.DistributionHostedZoneId
DNSName: !GetAtt MyApiDomainName.DistributionDomainName
I am getting this error: Cannot import certificates for REGIONAL while EDGE is active. (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: XXXXXXXXXXXXXXXXXXXXXXXX)
Although nothing I have is an EDGE Api Gateway and Nothing has the name I want (Stage.example.com)
Any help on this issue would be appreciated!
I came across the same error using Terraform, and the issue was that I was specifying regional_certificate_arn rather than certificate_arn. API Gateway defaults to edge-optimized, which means you're specifying a RegionalCertificateArn with an edge-optimized API here - try using CertificateArn instead.

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

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: XXX.execute-api.us-east-1.amazonaws.com/prod/outages
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. api.foo.com/outages resolves to XXX.execute-api.us-east-1.amazonaws.com/prod/outages)
This works fine if my lambda is NOT configured for my VPC
But when my lambda IS configured for my VPC, the custom domain api.foo.com/outages does NOT resolve to XXX.execute-api.us-east-1.amazonaws.com/prod/outages
I other words: I can NOT access api.foo.com/outages publicly.
What I need is:
1 - XXX.execute-api.us-east-1.amazonaws.com/prod/outages is available publicly (this works)
2 - My custom domain, api.foo.com/outages points to the SAME lambda as XXX.execute-api.us-east-1.amazonaws.com/prod/outages (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
I believe you might have missed to add VPC endpoints for API Gateway.
Ensure you create a VPC interface endpoint for API GW and use it in the API you create. This will allow API requests to lambda running within VPC
References:
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-private-apis.html#apigateway-private-api-create-interface-vpc-endpoint
https://aws.amazon.com/blogs/compute/introducing-amazon-api-gateway-private-endpoints/
Hope this helps !!
SOLUTION: We found a solution, in our case the solution is that you can't use wildcard certificates. So if you make a certificate for the exact API URL it does work. Obviously this is not a nice solution for everyone and everything but when you do have this problem and must deploy you can make it work this way.
Also maybe good to mention we did not have any additional problems with the below mentioned case:
I also wanted to add a post I found that might also have lead to this setup not working even if we do get the network to play nice. But again if you or anyone has a working setup like the above mentioned one let me know how you did it.
TLDR
If anyone wants to understand what was going on with API Gateway, take
a look at this thread.
It basically says that API Gateway processes regular URLs (like
aaaaaaaaaaaa.execute-api.us-east-1.amazonaws.com) differently than how
it processes Custom Domain Name URLs (like api.myservice.com). So when
API Gateway forwards your API request to your Lambda Function, your
Lambda Function will receive different path values, depending on which
type of your URL you used to invoke your API.
source:
Understanding AWS API Gateway Custom Domain Names