Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions examples/2016-10-31/custom_domains_with_route53/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Custom Domains support

Example SAM template for setting up Api Gateway resources for custom domains.

## Prerequisites for setting up custom domains
1. A domain name. You can purchase a domain name from a domain name provider.
1. A certificate ARN. Set up or import a valid certificate into AWS Certificate Manager. If the endpoint is EDGE, the certificate must be created in us-east-1.
1. A HostedZone in Route53 for the domain name.
1. A Cloudfront Distribution for the domain if the endpoint is set to EDGE.

## PostRequisites
After deploying the template, make sure you configure the DNS settings on the domain name provider's website. You will need to add Type A and Type AAAA DNS records that are point to ApiGateway's Hosted Zone Id. Read more [here](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/routing-to-api-gateway.html)

## Running the example

```bash
$ sam deploy \
--template-file /path_to_template/packaged-template.yaml \
--stack-name my-new-stack \
--capabilities CAPABILITY_IAM
```
70 changes: 70 additions & 0 deletions examples/2016-10-31/custom_domains_with_route53/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Parameters:
DomainName:
Type: String
Default: 'example.com'
ACMCertificateArn:
Type: String
Default: 'cert-arn-in-us-east-1'
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Handler: index.handler
Runtime: nodejs8.10
Events:
Fetch:
Type: Api
Properties:
RestApiId: !Ref MyApi
Method: Post
Path: /fetch

MyApi:
Type: AWS::Serverless::Api
Properties:
OpenApiVersion: 3.0.1
StageName: Prod
Domain:
DomainName: !Ref DomainName
CertificateArn: !Ref ACMCertificateArn
EndpointConfiguration: EDGE
BasePath:
- /fetch
Route53:
HostedZoneId: ZQ1UAL4EFZVME
IpV6: true
DistributionDomainName: !GetAtt Distribution.DomainName

Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !Ref DomainName
Id: !Ref DomainName
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
AllowedMethods: [ HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH ]
ForwardedValues:
QueryString: false
SmoothStreaming: false
Compress: true
TargetOriginId: !Ref DomainName
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_100
ViewerCertificate:
SslSupportMethod: sni-only
AcmCertificateArn: !Ref ACMCertificateArn
69 changes: 62 additions & 7 deletions samtranslator/model/api/api_generator.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from collections import namedtuple
from six import string_types
from samtranslator.model.intrinsics import ref
from samtranslator.model.intrinsics import ref, fnGetAtt
from samtranslator.model.apigateway import (ApiGatewayDeployment, ApiGatewayRestApi,
ApiGatewayStage, ApiGatewayAuthorizer,
ApiGatewayResponse, ApiGatewayDomainName,
ApiGatewayBasePathMapping)
from samtranslator.model.route53 import Route53RecordSetGroup
from samtranslator.model.exceptions import InvalidResourceException
from samtranslator.model.s3_utils.uri_parser import parse_s3_uri
from samtranslator.region_configuration import RegionConfiguration
Expand Down Expand Up @@ -218,23 +219,26 @@ def _construct_api_domain(self, rest_api):
Constructs and returns the ApiGateway Domain and BasepathMapping
"""
if self.domain is None:
return None, None
return None, None, None

if self.domain.get('DomainName') is None or \
self.domain.get('CertificateArn') is None:
raise InvalidResourceException(self.logical_id,
"Custom Domains only works if both DomainName and CertificateArn"
" are provided")

logical_id = logical_id_generator.LogicalIdGenerator("", self.domain).gen()
self.domain['ApiDomainName'] = "{}{}".format('ApiGatewayDomainName',
logical_id_generator.
LogicalIdGenerator("", self.domain.get('DomainName')).gen())

domain = ApiGatewayDomainName('ApiGatewayDomainName' + logical_id,
domain = ApiGatewayDomainName(self.domain.get('ApiDomainName'),
attributes=self.passthrough_resource_attributes)
domain.DomainName = self.domain.get('DomainName')
endpoint = self.domain.get('EndpointConfiguration')

if endpoint is None:
endpoint = 'REGIONAL'
self.domain['EndpointConfiguration'] = 'REGIONAL'
elif endpoint not in ['EDGE', 'REGIONAL']:
raise InvalidResourceException(self.logical_id,
"EndpointConfiguration for Custom Domains must be"
Expand Down Expand Up @@ -276,7 +280,58 @@ def _construct_api_domain(self, rest_api):
basepath_mapping.BasePath = path
basepath_resource_list.extend([basepath_mapping])

return domain, basepath_resource_list
# Create the Route53 RecordSetGroup resource
record_set_group = None
if self.domain.get('Route53') is not None:
route53 = self.domain.get('Route53')
if route53.get('HostedZoneId') is None:
raise InvalidResourceException(self.logical_id,
"HostedZoneId is required to enable Route53 support on Custom Domains.")
logical_id = logical_id_generator.LogicalIdGenerator("", route53.get('HostedZoneId')).gen()
record_set_group = Route53RecordSetGroup('RecordSetGroup' + logical_id,
attributes=self.passthrough_resource_attributes)
record_set_group.HostedZoneId = route53.get('HostedZoneId')
record_set_group.RecordSets = self._construct_record_sets_for_domain(self.domain)

return domain, basepath_resource_list, record_set_group

def _construct_record_sets_for_domain(self, domain):
recordset_list = []
recordset = {}
route53 = domain.get('Route53')

recordset['Name'] = domain.get('DomainName')
recordset['Type'] = 'A'
recordset['AliasTarget'] = self._construct_alias_target(self.domain)
recordset_list.extend([recordset])

recordset_ipv6 = {}
if route53.get('IpV6') is not None and route53.get('IpV6') is True:
recordset_ipv6['Name'] = domain.get('DomainName')
recordset_ipv6['Type'] = 'AAAA'
recordset_ipv6['AliasTarget'] = self._construct_alias_target(self.domain)
recordset_list.extend([recordset_ipv6])

return recordset_list

def _construct_alias_target(self, domain):
alias_target = {}
route53 = domain.get('Route53')
target_health = route53.get('EvaluateTargetHealth')

if target_health is not None:
alias_target['EvaluateTargetHealth'] = target_health
if domain.get('EndpointConfiguration') == 'REGIONAL':
alias_target['HostedZoneId'] = fnGetAtt(self.domain.get('ApiDomainName'), 'RegionalHostedZoneId')
alias_target['DNSName'] = fnGetAtt(self.domain.get('ApiDomainName'), 'RegionalDomainName')
else:
if route53.get('DistributionDomainName') is None:
raise InvalidResourceException(self.logical_id,
"Custom Domains support for EDGE requires the name "
"of a Distribution resource")
alias_target['HostedZoneId'] = 'Z2FDTNDATAQYW2'
alias_target['DNSName'] = route53.get('DistributionDomainName')
return alias_target

def to_cloudformation(self):
"""Generates CloudFormation resources from a SAM API resource
Expand All @@ -285,7 +340,7 @@ def to_cloudformation(self):
:rtype: tuple
"""
rest_api = self._construct_rest_api()
domain, basepath_mapping = self._construct_api_domain(rest_api)
domain, basepath_mapping, route53 = self._construct_api_domain(rest_api)
deployment = self._construct_deployment(rest_api)

swagger = None
Expand All @@ -297,7 +352,7 @@ def to_cloudformation(self):
stage = self._construct_stage(deployment, swagger)
permissions = self._construct_authorizer_lambda_permission()

return rest_api, deployment, stage, permissions, domain, basepath_mapping
return rest_api, deployment, stage, permissions, domain, basepath_mapping, route53

def _add_cors(self):
"""
Expand Down
10 changes: 10 additions & 0 deletions samtranslator/model/route53.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from samtranslator.model import PropertyType, Resource
from samtranslator.model.types import is_type, is_str


class Route53RecordSetGroup(Resource):
resource_type = 'AWS::Route53::RecordSetGroup'
property_types = {
'HostedZoneId': PropertyType(False, is_str()),
'RecordSets': PropertyType(False, is_type(list)),
}
7 changes: 5 additions & 2 deletions samtranslator/model/sam_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from .tags.resource_tagging import get_tag_list
from samtranslator.model import (PropertyType, SamResourceMacro,
ResourceTypeResolver)
from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayStage
from samtranslator.model.apigateway import ApiGatewayDeployment, ApiGatewayStage, ApiGatewayDomainName
from samtranslator.model.cloudformation import NestedStack
from samtranslator.model.dynamodb import DynamoDBTable
from samtranslator.model.exceptions import (InvalidEventException,
Expand Down Expand Up @@ -489,6 +489,7 @@ class SamApi(SamResourceMacro):
referable_properties = {
"Stage": ApiGatewayStage.resource_type,
"Deployment": ApiGatewayDeployment.resource_type,
"DomainName": ApiGatewayDomainName.resource_type
}

def to_cloudformation(self, **kwargs):
Expand Down Expand Up @@ -531,14 +532,16 @@ def to_cloudformation(self, **kwargs):
models=self.Models,
domain=self.Domain)

rest_api, deployment, stage, permissions, domain, basepath_mapping = api_generator.to_cloudformation()
rest_api, deployment, stage, permissions, domain, basepath_mapping, route53 = api_generator.to_cloudformation()

resources.extend([rest_api, deployment, stage])
resources.extend(permissions)
if domain:
resources.extend([domain])
if basepath_mapping:
resources.extend(basepath_mapping)
if route53:
resources.extend([route53])
return resources


Expand Down
70 changes: 70 additions & 0 deletions tests/translator/input/api_with_custom_domain_route53.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
Parameters:
DomainName:
Type: String
Default: 'example.com'
ACMCertificateArn:
Type: String
Default: 'cert-arn-in-us-east-1'
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Handler: index.handler
Runtime: nodejs8.10
Events:
Fetch:
Type: Api
Properties:
RestApiId: !Ref MyApi
Method: Post
Path: /fetch

MyApi:
Type: AWS::Serverless::Api
Properties:
OpenApiVersion: 3.0.1
StageName: Prod
Domain:
DomainName: !Ref DomainName
CertificateArn: !Ref ACMCertificateArn
EndpointConfiguration: EDGE
BasePath:
- /fetch
Route53:
HostedZoneId: ZQ1UAL4EFZVME
IpV6: true
DistributionDomainName: !GetAtt Distribution.DomainName

Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
HttpVersion: http2
Origins:
- DomainName: !Ref DomainName
Id: !Ref DomainName
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
DefaultCacheBehavior:
AllowedMethods: [ HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH ]
ForwardedValues:
QueryString: false
SmoothStreaming: false
Compress: true
TargetOriginId: !Ref DomainName
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_100
ViewerCertificate:
SslSupportMethod: sni-only
AcmCertificateArn: !Ref ACMCertificateArn
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
Resources:
MyFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async (event) => {
const response = {
statusCode: 200,
body: JSON.stringify('Hello from Lambda!'),
};
return response;
};
Handler: index.handler
Runtime: nodejs8.10
Events:
Api:
Type: Api
Properties:
RestApiId: !Ref MyApi
Method: Put
Path: /get
Fetch:
Type: Api
Properties:
RestApiId: !Ref MyApi
Method: Post
Path: /fetch

MyApi:
Type: AWS::Serverless::Api
Properties:
OpenApiVersion: 3.0.1
StageName: Prod
Domain:
DomainName: 'api-example.com'
CertificateArn: 'my-api-cert-arn'
EndpointConfiguration: 'EDGE'
BasePath: [ "/get", "/fetch" ]
Route53:
EvaluateTargetHealth: false
Loading