This article is intended for developers who want to deploy a Single Page Application on Cloudfront and make it accessible via a custom domain. I will try to make it systematic so its easier to pick and implement directly. I will try my best to explain why each step is necessary. Sometimes I have found that things don’t work and usually there’s a puny reason behind it. Therefore, knowing how things are working behind the scene could help resolve these issues quickly.
Table of Contents
Ground Setup
It’s always good to create a goal as to what exactly you are trying to achieve from this. And the best way is to do this is by defining the infrastructure on a white board or paper. Here’s the infrastructure that you will build using CDK.
Cloudfront
Cloudfront is a service offerred by AWS that sits between you and your webserver to serve you content with minimal latency. You can think of it as the nearest cache location also known as edge locations. In case the content is not cached in the nearest edge location, the cloudfront fetches the content from the origin server and returns to the user while caching it at the same time.
Route 53 DNS
This is a highly scalable Domain Name Server web service. You can use this service to perform 3 main functions:
- Domain Registration
- DNS routing
- Health Checking
In this tutorial, we will be using it for routing traffic to our deployed cloudfront destribution.
Infrastructure as Code using CDK
Before starting with the code the prerequisite is that you should have CDK code base setup and ready to go.
Easiest way to setup cdk project is by using CDK CLI. This is provided by AWS to make your life easier.
Create a folder
mkdir cdk-bma
Install CDK CLI
Navigate to the above folder and run following command:
npm install -g aws-cdk # install latest version
Initialize Project
cdk init app --language typescript
After running the above command, it will setup the initial project for you to start with right away. Following is the initial project setup:
.
├── bin
├── cdk.out
├── lib
├── node_modules
└── test
Now, just to get started, we will only write the infrastructure code in the lib/cdk-bma-stack.ts
file. This is the main stack that is created for us to get started.
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
// import * as sqs from 'aws-cdk-lib/aws-sqs';
export class CdkBmaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// The code that defines your stack goes here
// example resource
// const queue = new sqs.Queue(this, 'CdkBmaQueue', {
// visibilityTimeout: cdk.Duration.seconds(300)
// });
}
}
If you want to create more stacks or nested stacks you can take a look into the bin/cdk-bma.ts
file for reference.
Since the code supports typescript, so you can use any typescript construct to structure your infrastructure. And that is the power behind CDK compared to other infra tools like terraform. It is actual code that you write, no templating, pure code. You can use all your object oriented knowledge as well while structure your stacks and different modules.
Let’s dive into the code now.
Step 1: Create S3 Bucket to Host Single Page Application
const domainName = 'mywebsitedomain.com';
const prefix = "mywebsiteprefix";
export class CdkBmaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const websiteBucket = new aws_s3.Bucket(this, `${prefix}-bucket`, {
bucketName: `${prefix}-website-bucket`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
websiteIndexDocument: 'index.html',
});
.
.
.
}
Step 2: Add Policy to Allow Read Operation on S3 bucket
.
.
websiteBucket.addToResourcePolicy(new PolicyStatement({
sid: 's3BucketPublicRead',
effect: Effect.ALLOW,
actions: ['s3:GetObject'],
principals: [new ServicePrincipal('cloudfront.amazonaws.com')],
resources: [`${websiteBucket.bucketArn}/*`]
}))
.
.
Step 3: Creating an Origin Access Identity (OAI) and Grant Read Permission
An OAI is used by CloudFront
to securely access the content in your Amazon S3
bucket. Essentially, it acts as a virtual user identity that CloudFront uses to request files from your bucket.
After executing this code, CloudFront can use the OAI to securely access and serve files stored in the specified S3 bucket (websiteBucket
). This setup is commonly used for hosting static websites or content with CloudFront, where you want to restrict direct access to the S3 bucket and only allow access through CloudFront.
..
const oai = new aws_cloudfront.OriginAccessIdentity(this, "OriginAccessIdentity");
websiteBucket.grantRead(oai);
..
Step 4: Create SSL Certificate
const certificate = new aws_certificatemanager.Certificate(this, 'SiteCertificate', {
domainName: domainName,
validation: aws_certificatemanager.CertificateValidation.fromDns(hostedZone),
});
This is a 4 step process that CDK does for you with just 3 lines of code. But its good to understand what it does internally to make certificate available.
Certificate Request
- When you request a certificate from ACM and choose DNS validation, ACM generates two unique strings for each domain name that the certificate will cover. These strings are used to construct a CNAME record. One string serves as the name (or host) for the CNAME record, and the other as the value (or points to) the CNAME record.
CNAME Record Creation
- ACM provides you with the specific CNAME record details:
- Name: This is constructed using one of the unique strings generated by ACM and your domain name. It looks something like
_abc123.example.com
, where_abc123
is the unique string andexample.com
is your domain. - Value: This points to another unique string generated by ACM, indicating a domain under ACM’s control, such as
_xyz456.acm-validations.aws
.
- Name: This is constructed using one of the unique strings generated by ACM and your domain name. It looks something like
- You then create this CNAME record in your DNS configuration. If you’re using Amazon Route 53 and the domain is managed there, ACM can often add the record automatically if given permission.
Validation Process
- ACM periodically queries the DNS system for the CNAME record you were instructed to create. ACM knows exactly which CNAME record to look for because it generated the unique strings that compose the name and value of the record.
- By resolving the CNAME record to the expected value, ACM can confirm that you control the domain names for which you’re requesting the certificate. This is because only someone with control over the domain’s DNS settings could create the specific CNAME record ACM requested.
Certificate Issuance
- Once ACM successfully validates your control of the domain by finding and matching the CNAME record, the certificate’s status changes to “Issued,” and it becomes available for use in AWS services like Elastic Load Balancing, Amazon CloudFront, and API Gateway.
Step 5: Create Cloudfront Website Distribution
The code is pretty self explanatory. It does following:
- Instantiate a new Cloudfront Distribution
- Configure S3 bucket as the origin to fetch and serve content from the bucket
- Define error configurations
- Sets up viewer protocol policy to redirect all requests to HTTPS
- Configure cloudfront to use SSL Certificate (created in the prior step)
const cloudFrontDistribution = new aws_cloudfront.CloudFrontWebDistribution(this, `cloud-front-distribution`,{
originConfigs: [
{
s3OriginSource: {
s3BucketSource: websiteBucket,
originAccessIdentity: oai
},
behaviors: [{isDefaultBehavior: true}]
}
],
errorConfigurations: [
{
errorCode: 404,
responseCode: 200,
responsePagePath: '/404.html',
},
],
viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
viewerCertificate: aws_cloudfront.ViewerCertificate.fromAcmCertificate(certificate, {
aliases: [domainName],
securityPolicy: aws_cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019,
sslMethod: aws_cloudfront.SSLMethod.SNI
})
});
Step 6: Create Hosted Zone and Point to Cloudfront Alias
const hostedZone = new route53.PublicHostedZone(this, 'MyHostedZone', {
zoneName: domainName,
});
new route53.ARecord(this, 'CloudFrontARecord', {
zone: hostedZone,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(cloudFrontDistribution)),
recordName: 'www'
});
Step 7: Bucket Deployment to Serve Single Page Application
new aws_s3_deployment.BucketDeployment(this, `react-app-deployment`, {
destinationBucket: websiteBucket,
sources: [aws_s3_deployment.Source.asset("../build")],
distribution: cloudFrontDistribution,
distributionPaths: ["/*"]
});
Complete Infrastructure Code
import * as cdk from 'aws-cdk-lib';
import {
aws_apigateway,
aws_certificatemanager,
aws_cloudfront,
aws_iam,
aws_lambda,
aws_route53,
aws_route53_targets,
aws_s3,
aws_s3_deployment
} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {Effect, PolicyStatement, ServicePrincipal} from "aws-cdk-lib/aws-iam";
const domainName = 'mywebsitedomain.com';
const prefix = "mywebsiteprefix";
export class CdkBmaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const websiteBucket = new aws_s3.Bucket(this, `${prefix}-bucket`, {
bucketName: `${prefix}-website-bucket`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
websiteIndexDocument: 'index.html',
});
const oai = new aws_cloudfront.OriginAccessIdentity(this, "OriginAccessIdentity");
websiteBucket.grantRead(oai);
websiteBucket.addToResourcePolicy(new PolicyStatement({
sid: 's3BucketPublicRead',
effect: Effect.ALLOW,
actions: ['s3:GetObject'],
principals: [new ServicePrincipal('cloudfront.amazonaws.com')],
resources: [`${websiteBucket.bucketArn}/*`]
}))
// Create an SSL certificate
const certificate = new aws_certificatemanager.Certificate(this, 'SiteCertificate', {
domainName: domainName,
validation: aws_certificatemanager.CertificateValidation.fromDns(hostedZone),
});
const cloudFrontDistribution = new aws_cloudfront.CloudFrontWebDistribution(this, `cloud-front-distribution`,{
originConfigs: [
{
s3OriginSource: {
s3BucketSource: websiteBucket,
originAccessIdentity: oai
},
behaviors: [{isDefaultBehavior: true}]
}
],
errorConfigurations: [
{
errorCode: 404,
responseCode: 200,
responsePagePath: '/404.html',
},
],
viewerProtocolPolicy: aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
viewerCertificate: aws_cloudfront.ViewerCertificate.fromAcmCertificate(certificate, {
aliases: [domainName],
securityPolicy: aws_cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019,
sslMethod: aws_cloudfront.SSLMethod.SNI
})
});
const hostedZone = new route53.PublicHostedZone(this, 'MyHostedZone', {
zoneName: domainName,
});
new route53.ARecord(this, 'CloudFrontARecord', {
zone: hostedZone,
target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(cloudFrontDistribution)),
recordName: 'www'
});
new aws_s3_deployment.BucketDeployment(this, `react-app-deployment`, {
destinationBucket: websiteBucket,
sources: [aws_s3_deployment.Source.asset("../build")],
distribution: cloudFrontDistribution,
distributionPaths: ["/*"]
});
}
}
Step 8: Deploy
Its time to deploy our stack and see it in action.
cdk deploy CdkBmaStack
Conclusion
In this article we covered all the steps required to deploy any Single Page Application to S3 and host it via Cloudfront behind a DNS. We wrote the entire code in CDK using typescript to see the infra come to life. I would strongly recommend CDK when you are starting a new project because of the shear ease it brings in terms of managing infrastructure. All the infrastructure stays with you at all time.
Let me know how you find this article and definitely comment below if you face any problem. I would love to sort any of your queries and improve this article over time.