My goto serverless stack for super speed to production web applications, and RESTful APis, is CloudFront, S3, API Gateway, and Lambda. CloudFront provides the front door to the application, serving static content from S3 and providing paths to API Gateway APIs - no CORS here.
CloudFront can be leveraged for caching and routing to APIs, CMSes, etc. It’s a nice cheap setup with quite a lot of flexibility and integration with AWS services. This website is served by CloudFront and AWS CodePipeline provides the CI/CD. I prefer working with that flexibility, however if you want something light and quick you can check out AWS Amplify or Netflify.
This tutorial covers creating the base for this stack by serving static content using AWS CloudFront (the CDN), S3 (storage for the static content). The S3 bucket will be private with serverside encryption enabled by default. AWS Route 53 (DNS service) will be configured to have requests sent to a custom domain routed to the CloudFront distribution created in this tutorial.
The source code for this tutorial can be found here
What if I don’t want a custom domain
If no domain is required, and hitting the CloudFront distribution domain directly is adequate for your needs, then follow these instructions instead.
Costs
Warning! You will incur costs by following this tutorial.
Service | Cost |
---|---|
Domain name | .com goes for around €8 for one year (€11 to renew) with Namecheap |
AWS Route 53 hosted zone | 1 x €0.50 per month |
AWS CloudFront | Charged on data transfer out & http/https requests |
AWS S3 | You pay for storage, requests, and retrievals - Caching will make retrieval minimal |
Step 1: Register a Domain Name
You can use AWS Route 53 to register a domain name by following these instructions. Alternatively you can use your favourite domain registrar to register a domain. My favourite is Namecheap due to their low prices, customer support, and tooling.
This tutorial will use Route 53 to alias your domain name and the subdomain www
to route traffic to the CloudFront distribution e.g.
example.com
www.example.com
Step 2: Create a Hosted Zone
If you decided to use AWS Route 53 to register a domain name then a Route 53 hosted zone will already have been created for you. Take note of the Hosted Zone Id. Now go straight to step 4.
Otherwise, if you decided to use a domain registrar other than AWS Route 53 then create an AWS Route 53 hosted zone by using the following CloudFormation template:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DomainName:
Type: String
Resources:
HostedZone:
Type: AWS::Route53::HostedZone
Properties:
Name: !Ref DomainName
Outputs:
HostedZoneId:
Description: The Hosted Zone Id
Value: !Ref HostedZone
Export:
Name: !Sub ${AWS::StackName}-HostedZoneId
HostedZoneNameServers:
Description: The Hosted Zone Name Servers
Value: !Join
- ', '
- !GetAtt HostedZone.NameServers
Export:
Name: !Sub ${AWS::StackName}-HostedZoneNameServers
Use the following AWS CLI command to create the hosted zone:
aws cloudformation deploy \
--template-file hosted-zone.yaml \
--stack-name mystaticwebsite-hosted-zone \
--parameter-overrides \
DomainName=<your fully qualified domain name>
Run the following AWS CLI command to get the hosted zone id and the hosted zone nameservers:
aws cloudformation describe-stacks \
--stack-name mystaticwebsite-hosted-zone
Look for the "Outputs"
JSON element in the output of the above command. It should look like this:
"Outputs": [
{
"OutputKey": "HostedZoneId",
"OutputValue": "<this will be your hosted zone id>",
"Description": "The Hosted Zone Id",
"ExportName": "mystaticwebsite-hosted-zone-HostedZoneId"
},
{
"OutputKey": "HostedZoneNameServers",
"OutputValue": "<this will be a comma separated list of the nameservers associated with your hosted zone>",
"Description": "The Hosted Zone Name Servers",
"ExportName": "mystaticwebsite-hosted-zone-HostedZoneNameServers"
}
],
Take note of the hosted zone id, and the comma separated list of hosted zone name servers.
Step 3: Set Route 53 as the DNS service for the domain
To successfully have traffic to the domain routed to the CloudFront distribution, Route 53 needs to be set as the DNS service for the domain. To achieve this, the nameservers for the domain need to be set to the nameservers for the Route 53 hosted zone created in Step 2. The nameservers for the Route 53 hosted zone can be found in the comma separated list of the "OutputValue"
in the output from the CloudFormation describe CLI command in Step 2.
The domain registrar used to register the domain will have a console that can be used to update the nameservers for the domain. For example these are the instructions for Namecheap
Step 4: Create the Certificate
With the creation of the Route 53 hosted zone, and with Route 53 as the DNS service for the domain, the certificate for the static website and can be created and automatically validated by AWS Route 53. The following CloudFormation template creates the certificate using AWS ACM, however it must be created in the us-east-1
region otherwise it cannot be used with CloudFront:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DomainName:
Type: String
HostedZoneId:
Type: String
Resources:
Certificate:
Type: AWS::CertificateManager::Certificate
Properties:
DomainName: !Ref DomainName
SubjectAlternativeNames:
- !Ref DomainName
- !Sub www.${DomainName}
DomainValidationOptions:
- DomainName: !Ref DomainName
HostedZoneId: !Ref HostedZoneId
- DomainName: !Sub www.${DomainName}
HostedZoneId: !Ref HostedZoneId
ValidationMethod: DNS
Outputs:
CertificateArn:
Description: The Certificate ARN
Value: !Ref Certificate
Export:
Name: !Sub ${AWS::StackName}-CertificateArn
This is the AWS CLI command to create the stack:
aws cloudformation deploy \
--template-file acm-certificate.yaml \
--stack-name mystaticwebsite-acm-certificate \
--region us-east-1 \
--parameter-overrides \
DomainName=<your fully qualified domain name> \
HostedZoneId=<hosted zone id>
Notice the region is set to us-east-1
. This is necessary otherwise the certificate won’t work with CloudFront.
Once the stack is created, run the following command to get the certificate ARN, this is required when creating the CloudFront distribution.
aws cloudformation describe-stacks \
--region us-east-1 \
--stack-name mystaticwebsite-acm-certificate
The Outputs
JSON element will look like this:
"Outputs": [
{
"OutputKey": "CertificateArn",
"OutputValue": "arn:aws:acm:us-east-1:111111111111:certificate/b8d0e2c9-daf7-42e8-a59b-3693bc299c32",
"Description": "The Certificate ARN",
"ExportName": "mystaticwebsite-acm-certificate-CertificateArn"
}
],
Take note of the ARN which will look like this: arn:aws:acm:us-east-1:111111111111:certificate/b8d0e2c9-daf7-42e8-a59b-3693bc299c32
Step 5: Create The CloudFront Distribution
The following CloudFormation template creates the Origin Access Identity, static resources S3 bucket, and CloudFront distribution:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DomainName:
Type: String
CertificateArn:
Type: String
Resources:
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub ${AWS::StackName}-s3-origin-oai
StaticResourcesBucket:
Type: AWS::S3::Bucket
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
StaticResourcesBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref StaticResourcesBucket
PolicyDocument:
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}
Action: s3:GetObject
Resource: !Sub arn:aws:s3:::${StaticResourcesBucket}/*
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Aliases:
- !Ref DomainName
- !Sub www.${DomainName}
Origins:
- DomainName: !Sub ${StaticResourcesBucket}.s3.${AWS::Region}.amazonaws.com
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
TargetOriginId: S3Origin
ForwardedValues:
QueryString: false
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
PriceClass: PriceClass_All
ViewerCertificate:
AcmCertificateArn: !Ref CertificateArn
SslSupportMethod: sni-only
Outputs:
DistributionId:
Description: CloudFront Distribution Id
Value: !Ref Distribution
Export:
Name: !Sub ${AWS::StackName}-DistributionId
DistributionDomainName:
Description: CloudFront Distribution Domain Name
Value: !GetAtt Distribution.DomainName
Export:
Name: !Sub ${AWS::StackName}-DistributionDomainName
StaticResourcesBucketName:
Description: Static Resources Bucket Name
Value: !Ref StaticResourcesBucket
Export:
Name: !Sub ${AWS::StackName}-StaticResourcesBucketName
Run the following command to create the CloudFormation stack:
aws cloudformation deploy \
--template-file cloudfront-distribution.yaml \
--stack-name mystaticwebsite-cloudfront-distribution \
--parameter-overrides \
DomainName=<your domain name> \
CertificateArn=<certificate arn>
Origin Access Identity
The Origin Access Identity allows CloudFront to read from the S3 bucket without having to make the S3 bucket public.
S3 Bucket
The static content is stored in the S3 bucket from which CloudFront will serve the content. My preference is to encrypt all data whether it’s at rest or in-flight, so here serverside encryption is enabled by default. It’s data that will be publicly accessible through CloudFront, so the default AWS S3 key would appear to be adequate here despite it being a shared key across AWS accounts.
The S3 bucket is explicitly configured as private with the PublicAccessBlockConfiguration
.
S3 Bucket Policy
It is necessary to give s3:GetObject
permission to the Origin Access Identity so that CloudFront can request items from the S3 bucket. This means that the bucket can be kept private but that CloudFront can still access the static content within.
Cache Policy
The cache policy id for the default cache behaviour is set to the managed cache policy 658327ea-f89d-4fab-a63d-7e88639e58f6
- Managed-CachingOptimized - which means caching is enabled and CloudFront cache invalidation is required after making changes to static content files.
Try It Out
It should now be possible test the CloudFront distribution. Upload an index.html
file to the S3 bucket then send an HTTP request to the CloudFront URL. The name of the S3 bucket and CloudFront URL are required. Run the following command to get the CloudFront distribution stack outputs:
aws cloudformation describe-stacks \
--stack-name mystaticwebsite-cloudfront-distribution
The output should look like the following:
"Outputs": [
{
"OutputKey": "DistributionId",
"OutputValue": "<distribution Id>",
"Description": "CloudFront Distribution Id",
"ExportName": "mystaticwebsite-cloudfront-distribution-DistributionId"
},
{
"OutputKey": "DistributionDomainName",
"OutputValue": "d1111111111111.cloudfront.net",
"Description": "CloudFront Distribution Domain Name",
"ExportName": "mystaticwebsite-cloudfront-distribution-DistributionDomainName"
},
{
"OutputKey": "StaticResourcesBucketName",
"OutputValue": "mystaticwebsite-cloudfront-staticresourcesbucket-1ab0a0a0a9abc",
"Description": "Static Resources Bucket Name",
"ExportName": "mystaticwebsite-cloudfront-distribution-StaticResourcesBucketName"
}
],
Take note of the OutputValue
of "OutputKey": "DistributionDomainName"
and "OutputKey": "StaticResourcesBucketName"
.
Run the following command to create a file name index.html
and upload it to the S3 bucket:
echo 'My Static Content' > index.html && \
aws s3 cp index.html s3://<static resources bucket name>
With a browser, request the CloudFront URL taken from the stack output to see the content returned from CloudFront. Alternatively the following cURL command can be used:
curl https://<cloudfront distribution domain>
The response should look like this:
My Static Content
Step 6: Route Traffic To The CloudFront Distribution
A further step is required to have requests to the domain routed to the CloudFront distribution. It requires Route 53 alias records to be created:
- IPV4 alias record (A) pointing the root domain to the CloudFront distribution
- IPV4 alias record (A) pointing the
www
subdomain to the CloudFront distribution - IPV6 alias record (AAAA) pointing the root domain to the CloudFront distribution
- IPV6 alias record (AAAA) pointing the
www
subdomain to the CloudFront distribution
The following CloudFormation template sets up the appropriate records:
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DomainName:
Type: String
HostedZoneId:
Type: String
DistributionDomainName:
Type: String
Resources:
HostedZoneRecordSetGroup:
Type: AWS::Route53::RecordSetGroup
Properties:
HostedZoneId: !Ref HostedZoneId
RecordSets:
- Name: !Ref DomainName
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !Ref DistributionDomainName
- Name: !Sub www.${DomainName}
Type: A
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !Ref DistributionDomainName
- Name: !Ref DomainName
Type: AAAA
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !Ref DistributionDomainName
- Name: !Sub www.${DomainName}
Type: AAAA
AliasTarget:
HostedZoneId: Z2FDTNDATAQYW2
DNSName: !Ref DistributionDomainName
Run the following command to create the CloudFormation stack:
aws cloudformation deploy \
--template-file mystaticwebsite-record-set-group.yaml \
--stack-name mystaticwebsite-record-set-group \
--parameter-overrides \
DomainName=<your domain name> \
HostedZoneId=<hosted zone id> \
DistributionDomainName=<cloudfront distribution domain name>
Try it Out
Navigate to the domain name in a browser or run the following command:
curl https://<your domain name>
The following should be returned:
My Static Content
No domain no problem
If no domain is required, and hitting the CloudFront distribution domain directly is adequate for your needs, then this single CloudFormation template will suffice:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
OriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Sub ${AWS::StackName}-s3-origin-oai
StaticResourcesBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-static-resources
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
StaticResourcesBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref StaticResourcesBucket
PolicyDocument:
Statement:
- Effect: Allow
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${OriginAccessIdentity}
Action: s3:GetObject
Resource: !Sub arn:aws:s3:::${StaticResourcesBucket}/*
Distribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !Sub ${StaticResourcesBucket}.s3.${AWS::Region}.amazonaws.com
Id: S3Origin
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${OriginAccessIdentity}
Enabled: true
DefaultRootObject: index.html
DefaultCacheBehavior:
AllowedMethods:
- DELETE
- GET
- HEAD
- OPTIONS
- PATCH
- POST
- PUT
TargetOriginId: S3Origin
ForwardedValues:
QueryString: false
Cookies:
Forward: none
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
PriceClass: PriceClass_All
Outputs:
DistributionId:
Description: CloudFront Distribution Id
Value: !Ref Distribution
Export:
Name: !Sub ${AWS::StackName}-DistributionId
DistributionDomainName:
Description: CloudFront Distribution Domain Name
Value: !GetAtt Distribution.DomainName
Export:
Name: !Sub ${AWS::StackName}-DistributionDomainName
StaticResourcesBucketName:
Description: Static Resources Bucket Name
Value: !Ref StaticResourcesBucket
Export:
Name: !Sub ${AWS::StackName}-StaticResourcesBucketName
Run the following command to create the CloudFormation stack:
aws cloudformation deploy \
--template-file cloudfront-distribution-no-custom-domain.yaml \
--stack-name mystaticwebsite-cloudfront-distribution
Run the following command to retrieve the CloudFront distribution domain:
aws cloudformation describe-stacks \
--stack-name mystaticwebsite-cloudfront-distribution
The output should look like the following:
"Outputs": [
{
"OutputKey": "DistributionId",
"OutputValue": "<distribution Id>",
"Description": "CloudFront Distribution Id",
"ExportName": "mystaticwebsite-cloudfront-distribution-DistributionId"
},
{
"OutputKey": "DistributionDomainName",
"OutputValue": "d1111111111111.cloudfront.net",
"Description": "CloudFront Distribution Domain Name",
"ExportName": "mystaticwebsite-cloudfront-distribution-DistributionDomainName"
},
{
"OutputKey": "StaticResourcesBucketName",
"OutputValue": "mystaticwebsite-cloudfront-staticresourcesbucket-1ab0a0a0a9abc",
"Description": "Static Resources Bucket Name",
"ExportName": "mystaticwebsite-cloudfront-distribution-StaticResourcesBucketName"
}
],
Take note of the OutputValue
of "OutputKey": "DistributionDomainName"
and "OutputKey": "StaticResourcesBucketName"
.
Run the following command to create a file name index.html
and upload it to the S3 bucket:
echo 'My Static Content' > index.html && \
aws s3 cp index.html s3://<static resources bucket name>
With a browser, request the CloudFront URL taken from the stack output to see the content returned from CloudFront. Alternatively the following cURL command can be used:
curl https://<cloudfront distribution domain>
The response should look like this:
My Static Content