How to reference CloudFront domain name when creating a AWS::S3::RecordSet in CloudFormation/Serverless? - aws-cloudformation

I have a project which has a cloudfront distribution to serve some data out of a bucket. I am using Serverless framework, but I think this is mainly a CloudFormation question.
I would like to create the A record in a Route53 hosted domain (third level domain if that matters, ie: dashboard.domain.com is pointed to Route53, and I'm trying to add .dashboard.domain.com).
I just cannot figure out how to reference the output from the CloudFront resource?
This is what I have right now, and it works because it's all static. However, I need to automatically put in the correct cloud front domain which will be created by another resource. I figure these is some type of GetAttr I can do, but I just cannot get it to work.
DNSRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: Z09193931V4YGJEPVMLG1
RecordSets:
- Name: prod.dashboard.domain.com
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: someid.cloudfront.net
WebAppCloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName:
Fn::Join: [
"", [
{ "Ref": "WebAppS3Bucket" },
".s3.amazonaws.com"
]
]
## An identifier for the origin which must be unique within the distribution
Id: WebApp
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
Enabled: 'true'
## Uncomment the following section in case you are using a custom domain
Aliases:
- ${self:provider.stage}.dashboard.domain.com
DefaultRootObject: index.html
## Since the Single Page App is taking care of the routing we need to make sure ever path is served with index.html
## The only exception are files that actually exist e.h. app.js, reset.css
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
## The origin id defined above
TargetOriginId: WebApp
## Defining if and how the QueryString and Cookies are forwarded to the origin which in this case is S3
ForwardedValues:
QueryString: 'false'
Cookies:
Forward: none
## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
ViewerProtocolPolicy: redirect-to-https
## The certificate to use when viewers use HTTPS to request objects.
ViewerCertificate:
AcmCertificateArn:
Ref: SSLCertificate
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1
EDIT: Updated to include the WebAppCloudFrontDistribution

You haven't provided your AWS::CloudFront::Distribution resource definition, so I only can based it on an example.
MyCloudFrontDistro:
Type: AWS::CloudFront::Distribution
Properties:
# some properties
Then you can modify your DNSRecords
DNSRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: Z09193931V4YGJEPVMLG1
RecordSets:
- Name: prod.dashboard.domain.com
Type: A
AliasTarget:
HostedZoneId: !Ref MyCloudFrontDistro
DNSName: !GetAtt MyCloudFrontDistro.DomainName

WebAppCloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName:
Fn::Join: [
"", [
{ "Ref": "WebAppS3Bucket" },
".s3.amazonaws.com"
]
]
## An identifier for the origin which must be unique within the distribution
Id: WebApp
CustomOriginConfig:
HTTPPort: 80
HTTPSPort: 443
OriginProtocolPolicy: https-only
Enabled: 'true'
Aliases:
- ${self:provider.stage}.dashboard.domain.com
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
TargetOriginId: WebApp
ForwardedValues:
QueryString: 'false'
Cookies:
Forward: none
## The protocol that users can use to access the files in the origin. To allow HTTP use `allow-all`
ViewerProtocolPolicy: redirect-to-https
## The certificate to use when viewers use HTTPS to request objects.
ViewerCertificate:
AcmCertificateArn:
Ref: SSLCertificate
SslSupportMethod: sni-only
MinimumProtocolVersion: TLSv1
## Uncomment the following section in case you want to enable logging for CloudFront requests
# Logging:
# IncludeCookies: 'false'
# Bucket: mylogs.s3.amazonaws.com
# Prefix: myprefix
Resources:
DNSRecords:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneName: dashboard.domain.com.
RecordSets:
- Name: ${self:provider.stage}.dashboard.domain.com
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt WebAppCloudFrontDistribution.DomainName
Here is the working solution for me, take note of some points.
The HostedZoneId of Z2FDTNDATAQYW2 is special for the cloudfront domain. It needs to be used when referencing a cloud front resource.
The trailing space needs to be included on the HostedZoneName (if you use that compared to the HostedZoneId). In my case, I have the domain setup prior to the Cloud Formation.

Related

Custom DNS for Cloudfront returns 403

I am using Cloudformation to deploy resources. This includes a Cloudfront CDN and a RecordSet. I want this to create a new record set for a custom domain test.example.com, that will point to my Cloudfront CDN, which points to an S3 bucket. After successful deployment, I get the following error:
403 ERROR
The request could not be satisfied.
Bad request. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.
Testing using the Cloudfront Domain is successful. When I check the AWS console, the only thing I notice is that the Alternate Domain Names (CNAMEs) is empty:
When I populate that with the expected value test.example.com, the page loads just fine.
I have tried adding Aliases: test.example.com to the Cloudfront DistributionConfig but then I get a circular error with the RecordSet.
How can I have both the CDN and the RecordSet in Cloudformation, with the Alternate Domain Names (CNAMEs) populated?
Cloudfront:
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
-
# Use the Website as the origin
DomainName: !GetAtt 'Website.DomainName'
Id: !Ref Website
S3OriginConfig:
OriginAccessIdentity: !Join [ '', [ 'origin-access-identity/cloudfront/', !Ref CloudFrontOriginAccessIdentity] ]
Enabled: true
HttpVersion: http2
DefaultRootObject: index.html
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
- ErrorCode: 403
ResponseCode: 200
ResponsePagePath: /index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
DefaultTTL: 60
ForwardedValues:
QueryString: true
Cookies:
Forward: none
# The origin id defined above
TargetOriginId: !Ref Website
ViewerProtocolPolicy: "redirect-to-https" # we want to force https
# The certificate to use when using https
ViewerCertificate:
AcmCertificateArn: arn:aws:acm:us-east-1:<id>:certificate/<certId>
MinimumProtocolVersion: TLSv1
SslSupportMethod: sni-only
DNS:
DNS:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: <<id>>
RecordSets:
- Name: test.example.com
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !GetAtt
- Distribution
- DomainName
Looks like it is successful when I add the following to the Cloudfront distribution:
Aliases:
- test.example.com
My mistake is I was previously referencing the DNS:
Aliases: !Ref DNS
which was incorrect and causing my circular dependency issue

What is the format to specify an external EDGE Gateway ApiId in CloudFormation templates?

I'm trying to create or update a stack with the following CloudFormation Template:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
ApiGatewayId:
Type: String
ApiLayerArn:
Type: String
JarLocation:
Type: String
Resources:
Function:
Type: 'AWS::Lambda::Function'
Properties:
Handler: net.bitsandpaper.api.kiosk.PlatformChecker
Runtime: java11
Code:
S3Bucket: bnp-build-artifacts
S3Key: !Ref JarLocation
Description: ''
MemorySize: 128
Timeout: 5
Role: arn:aws:iam::479832603967:role/bnp-api-lambda-execution-role
Layers:
- !Ref ApiLayerArn
ApiIntegration:
Type: AWS::ApiGatewayV2::Integration
Properties:
ApiId: !Ref ApiGatewayId
IntegrationType: AWS_PROXY
IntegrationUri: !Join
- ''
- - 'arn:'
- !Ref 'AWS::Partition'
- ':apigateway:'
- !Ref 'AWS::Region'
- ':lambda:path/2015-03-31/functions/'
- !Ref Function
- /invocations
TimeoutInMillis: 6000
ApiRoute:
Type: AWS::ApiGatewayV2::Route
Properties:
ApiId: !Ref ApiGatewayId
RouteKey: 'GET /kiosk/platform-check'
Target: !Join
- /
- - integrations
- !Ref ApiIntegration
The parameters are correctly passed by an external file, they look good in the Web Console, notably parameter ApiGatewayId has value 8548rqrsm5. Yet during deployment I have a CREATE_FAILED for ApiIntegration, with the message:
Invalid API identifier specified 479832603967:8548rqrsm5 (Service:
AmazonApiGatewayV2; Status Code: 404; Error Code: NotFoundException;
Request ID: 84918a83-cf9d-48d2-acf7-18d9d2e4d330; Proxy: null)
The API is an EDGE Rest API, in the same region than the CloudFormation stack. The ID is retrieved by the CLI with aws apigateway get-rest-apis.
Am I missing something in the ApiId format? The litterature is very scarce when not referencing an API in the same stack...
AWS::ApiGatewayV2 is only for WEBSOCKTES and HTTP types. From docs:
The API protocol. Valid values are WEBSOCKET or HTTP.
But since you are writing about Edge-optimized (not supported by HTTP api) it seems to that you are using REST API, rather then HTTP API. So you should be using AWS::ApiGateway resources, not AWS::ApiGatewayV2.
It seem's like the AWS::ApiGatewayV2::Route is created before the AWS::ApiGatewayV2::Integration. So When it trying to refer ApiIntegration it is not yet created.
So you should try to use DependsOn attribute.
With the DependsOn attribute you can specify that the creation of a
specific resource follows another. When you add a DependsOn attribute
to a resource, that resource is created only after the creation of the
resource specified in the DependsOn attribute.
Try this below CloudFormation code:
ApiRoute:
Type: AWS::ApiGatewayV2::Route
DependsOn: ApiIntegration
Properties:
ApiId: !Ref ApiGatewayId
RouteKey: 'GET /kiosk/platform-check'
Target: !Join
- /
- - integrations
- !Ref ApiIntegration
I hope this will help you out to resolve your problem.
Link: DependsOn Attribute UserGuide

Cloudformation circular dependency - Route53 and Cloudfront

Trying to configure a Route53 and Cloudfront resource but they seem to have an implicit circular dependency on each other. Is there an elegant solution for it?
The snippet of my cloudformation template is below:
AppDnsRecord:
Type: "AWS::Route53::RecordSetGroup"
DependsOn: CloudFrontDistributionApp
Properties:
HostedZoneName: example.com.
RecordSets:
- Name: !Sub "app-${StackName}.example.com."
Type: CNAME
TTL: "60"
ResourceRecords:
- !GetAtt "CloudFrontDistributionApp.DomainName"
CloudFrontDistributionApp:
Type: "AWS::CloudFront::Distribution"
Properties:
DistributionConfig:
Aliases:
- !Sub "app-${StackName}.example.com"
# ...
The issue is that the CNAME record depends on the Cloudfront's domain name and has a dependsOn. However the Cloudfront's Aliases configuration requires the CNAME to exist.

API Gateway HTTP Proxy integration with serverless-offline (NOT Lambda Proxy)

I am trying to use serverless-offline to develop / simulate my API Gateway locally. My API gateway makes liberal use of the HTTP proxy integrations. The production Resource looks like this:
I have created a serverless-offline configuration based on a few documents and discussion which say that it is possible to define an HTTP Proxy integration using Cloud Formation configuration:
httpProxyWithApiGateway.md - Setting an HTTP Proxy on API Gateway by using Serverless framework.
Setting an HTTP Proxy on API Gateway (official Serverless docs: API Gateway)
I have adapted the above two configuration examples for my purposes, see below.
Have any tips, for what I might be doing wrong here?
plugins:
- serverless-offline
service: company-apig
provider:
name: aws
stage: dev
runtime: python2.7
resources:
Resources:
# Parent APIG RestApi
ApiGatewayRestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: company-apig
Description: 'The main entry point of the APIG'
# Resource /endpoint
EndpointResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Fn::GetAtt:
- ApiGatewayRestApi
- RootResourceId
PathPart: 'endpoint'
RestApiId:
Ref: ApiGatewayRestApi
# Resource /endpoint/{proxy+}
EndpointProxyPath:
Type: AWS::ApiGateway::Resource
Properties:
ParentId:
Ref: EndpointResource
PathPart: '{proxy+}'
RestApiId:
Ref: ApiGatewayRestApi
# Method ANY /endpoint/{proxy+}
EndpointProxyAnyMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
HttpMethod: ANY
Integration:
IntegrationHttpMethod: ANY
Type: HTTP_PROXY
Uri: http://endpoint.company.cool/{proxy}
PassthroughBehavior: WHEN_NO_MATCH
MethodResponses:
- StatusCode: 200
ResourceId:
Ref: EndpointProxyPath
RestApiId:
Ref: ApiGatewayRestApi
For the above configuration, I get this output. Apparently, the configuration registers no routes at all.
{
"statusCode":404,
"error":"Serverless-offline: route not found.",
"currentRoute":"get - /endpoint/ping",
"existingRoutes":[]
}
Related: I am also attempting to solve the same problem using aws-sam, at the following post - API Gateway HTTP Proxy integration with aws-sam (NOT Lambda Proxy)
By default serverless-offline doesn't parse your resources for endpoints, enable it via custom config.
custom:
serverless-offline:
resourceRoutes: true
Ends up serving:
Serverless: Routes defined in resources:
Serverless: ANY /endpoint/{proxy*} -> http://endpoint.company.cool/{proxy}
Serverless: Offline listening on http://localhost:3000
Documentation

AWS ApiGateway Proxy to non-Public S3 Bucket

The following cloudformation script sets up an Api Gateway method that proxies to an S3 bucket.
The S3BucketPolicy opens up the bucket to public read access but the AWS UI warns that this should never be done.
I tried setting the S3BucketPolicy Principal to service apigateway.amazonaws.com but this results in Access Denied.
1) What is the right way to limit bucket access to the API gateway function? (Sample YAML would be great)
2) How could I debug this Access Denied failure to get more information on why it failed?
3) Where should I be looking for sample code on what should be a very standard template snippet?
ATTEMPT #1 - Works but only by making the S3 Bucket Public otherwise Access Denied
AWSTemplateFormatVersion: 2010-09-09
Parameters:
S3BucketName:
Type: String
Description: >
Name for the S3 bucket that contains the nested templates.
Resources:
RestAPI:
Type: 'AWS::ApiGateway::RestApi'
Properties:
BinaryMediaTypes:
- '*/*'
Name: !Ref 'AWS::StackName'
RestAPIRootGET:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: GET
Integration:
IntegrationHttpMethod: GET
PassthroughBehavior: WHEN_NO_MATCH
Type: HTTP_PROXY
Uri: !Sub https://${S3BucketName}.s3.amazonaws.com/static-assets/index.html
ResourceId: !GetAtt RestAPI.RootResourceId
RestApiId: !Ref RestAPI
DependsOn:
- RestAPI
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3BucketName
PolicyDocument:
Version: 2012-10-17
Statement:
-
Sid: GetObject
Effect: Allow
Principal: "*"
Action:
- s3:*
Resource:
- !Sub 'arn:aws:s3:::${S3BucketName}/static-assets/*'
I think perhaps the right away is to create a role with access to the bucket and then have the ApiGateway assume this role but I'm having a hard time finding documentation that explains how to do this in a cloudformation template. (see also Michael - sqlbot comment suggesting using the credentials property of the method)
Here is my attempt which still fails with Access Denied
ATTEMPT #2 - Access Denied
AWSTemplateFormatVersion: 2010-09-09
Parameters:
S3BucketName:
Type: String
Description: >
Name for the S3 bucket that contains the nested templates.
Resources:
RestAPI:
Type: 'AWS::ApiGateway::RestApi'
Properties:
BinaryMediaTypes:
- '*/*'
Name: !Ref 'AWS::StackName'
RestAPIRootGET:
Type: 'AWS::ApiGateway::Method'
Properties:
AuthorizationType: NONE
HttpMethod: GET
Integration:
IntegrationHttpMethod: GET
PassthroughBehavior: WHEN_NO_MATCH
Type: HTTP_PROXY
Uri: !Sub https://${S3BucketName}.s3.amazonaws.com/static-assets/index.html
Credentials: !GetAtt AllowStaticAccessRole.Arn
ResourceId: !GetAtt RestAPI.RootResourceId
RestApiId: !Ref RestAPI
DependsOn:
- RestAPI
- AllowStaticAccessRole
AllowStaticAccessRole:
Type: "AWS::IAM::Role"
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Principal:
Service:
- "apigateway.amazonaws.com"
Action:
- "sts:AssumeRole"
Path: "/"
Policies:
-
PolicyName: "AllowStaticAccessPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
-
Effect: "Allow"
Action:
- s3:*
Resource:
- !Sub 'arn:aws:s3:::${S3BucketName}/static-assets/*'
As already mentioned in the comments, the Access Denied error could be coming from KMS rather than from the S3 itself.
To solve the issue, you need to add at minimum these permissions to the role that the Api Gateway assumes: "kms:Decrypt", "kms:ReEncryptFrom", preferably also stating which Resource this should apply too for proper least privilege implementation.