• Skip to main content
  • Skip to footer
  • Home
  • Become An Aficionado (Join Our Mailing List)
BMA

BeMyAficionado

Inspire Affection

Ad example

A Step-by-Step Guide to Deploy a Static Website with CloudFront and S3 Using CDK Behind A Custom Domain

February 6, 2024 by varunshrivastava Leave a Comment

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
    • Cloudfront
    • Route 53 DNS
  • Infrastructure as Code using CDK
    • Create a folder
    • Install CDK CLI
    • Initialize Project
    • Step 1: Create S3 Bucket to Host Single Page Application
    • Step 2: Add Policy to Allow Read Operation on S3 bucket
    • Step 3: Creating an Origin Access Identity (OAI) and Grant Read Permission
    • Step 4: Create SSL Certificate
      • Certificate Request
      • CNAME Record Creation
      • Validation Process
      • Certificate Issuance
    • Step 5: Create Cloudfront Website Distribution
    • Step 6: Create Hosted Zone and Point to Cloudfront Alias
    • Step 7: Bucket Deployment to Serve Single Page Application
    • Complete Infrastructure Code
    • Step 8: Deploy
  • Conclusion

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 and example.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.
  • 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.

Related

Filed Under: Blogging, Tutorials

Footer

Become an Aficionado

BeMyAficionado is all about helping you connect, grow, and make an impact—one idea at a time.

Join our mailing list for exclusive how-to guides, insider stories, and free e-books.

Get first access to new posts, tools and resources we only share with subscribers.

Join 874 other subscribers

Recent

  • Is The Cosmos a Vast Computation?
  • Building Semantic Search for E-commerce Using Product Embeddings and OpenSearch
  • Leader Election with ZooKeeper: Simplifying Distributed Systems Management
  • AWS Serverless Event Driven Data Ingestion from Multiple and Diverse Sources
  • A Step-by-Step Guide to Deploy a Static Website with CloudFront and S3 Using CDK Behind A Custom Domain

Search

Tags

Affordable Hosting algorithms amazon aoc-2020 believe in yourself best database earn money blogging education elementary sorting algorithms experience fashion finance Financial Freedom food friends goals google india indian cuisine indian education system java life life changing love make money microservices motivation oops podcast poor education system principles of microservices problem-solving programmer programming python reality seo spring success success factor technology top 5 typescript wordpress

Copyright © 2025 · Be My Aficionado · Log in

Go to mobile version