DEV Community

Cover image for Deploy your static React app to AWS Cloudfront using CDK
Paul Allies
Paul Allies

Posted on

Deploy your static React app to AWS Cloudfront using CDK

Here we illustrate how to do the following

  1. Create React App
  2. Setup CDK
  3. Get the AWS Hosted Zone
  4. Create S3 bucket for react app
  5. Create Certificate
  6. Create Cloudfront Distribution with certificate
  7. Add Route53 A Record for react app to target Cloudfront distribution
  8. Deploy react app

1. Create React App

$> npx create-react-app reactapp.nanosoft.co.za 
Enter fullscreen mode Exit fullscreen mode

To test, cd into the application folder and run

$> npm start 
Enter fullscreen mode Exit fullscreen mode

You should see the following appear

Screenshot 2021-07-30 at 09.54.17

2. Setup CDK

To deploy our application to AWS using CDK, we need to install the following dependencies:

npm i aws-cdk \ @aws-cdk/core \ @aws-cdk/aws-certificatemanager \ @aws-cdk/aws-cloudfront \ @aws-cdk/aws-route53 \ @aws-cdk/aws-route53-targets \ @aws-cdk/aws-s3 \ @aws-cdk/aws-s3-deployment 
Enter fullscreen mode Exit fullscreen mode

and the following dev dependency:

npm i -D @types/node \ typescript \ source-map-support 
Enter fullscreen mode Exit fullscreen mode

Within the same application root folder, create a cdk folder and a cdk.json file and here we'll write our infrastructure code.

Screenshot 2021-07-30 at 11.58.40

Within the cdk folder create 2 files:

├── cdk │ ├── index.ts │ └── stack.ts ├── cdk.json 
Enter fullscreen mode Exit fullscreen mode

and add the following to the cdk.json file

 { "app": "node cdk/index.js" } 
Enter fullscreen mode Exit fullscreen mode
//stack.ts import * as cdk from '@aws-cdk/core'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWSACCOUNTID>", region: "<REGION>" } }); } } 
Enter fullscreen mode Exit fullscreen mode
//index.ts #!/usr/bin/env node import * as cdk from '@aws-cdk/core'; import { Stack } from './stack'; const app = new cdk.App(); new Stack(app, 'ReactAppStack'); 
Enter fullscreen mode Exit fullscreen mode

let's add a build and deploy script to build our typescript infra code and deploy result infra to AWS:

 "cdk-build": "tsc --target ES2018 --moduleResolution node --module commonjs cdk/index.ts", "deploy": "npm run cdk_build && cdk deploy" 
Enter fullscreen mode Exit fullscreen mode

now run the deploy script

$> npm run deploy 
Enter fullscreen mode Exit fullscreen mode

to see the following output

> reactapp.nanosoft.co.za@0.1.0 deploy > npm run cdk-build && cdk deploy > reactapp.nanosoft.co.za@0.1.0 cdk-build > tsc --target ES2018 --moduleResolution node --module commonjs cdk/index.ts ReactAppStack: deploying... ✅ ReactAppStack (no changes) Stack ARN: arn:aws:cloudformation:af-south-1:80XXXXXXX:stack/ReactAppStack/7d3xxxx-xxx-xxxx-xxxx-061xxxxxxxx 
Enter fullscreen mode Exit fullscreen mode

3. Get the AWS Hosted Zone

import * as cdk from '@aws-cdk/core'; import * as route53 from '@aws-cdk/aws-route53'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWSACCOUNTID>", region: "<REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); console.log(zone.zoneName); } } 
Enter fullscreen mode Exit fullscreen mode

4. Create S3 bucket for react app

 import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as route53 from '@aws-cdk/aws-route53'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWS_ACCOUNT_ID>", region: "<AWS_REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); //Create S3 Bucket for our website const siteBucket = new s3.Bucket(this, "SiteBucket", { bucketName: WEB_APP_DOMAIN, websiteIndexDocument: "index.html", publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.DESTROY }) } } 
Enter fullscreen mode Exit fullscreen mode

5. Create Certificate

 import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as route53 from '@aws-cdk/aws-route53'; import * as acm from '@aws-cdk/aws-certificatemanager'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWS_ACCOUNT_ID>", region: "<AWS_REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); //Create S3 Bucket for our website const siteBucket = new s3.Bucket(this, "SiteBucket", { bucketName: WEB_APP_DOMAIN, websiteIndexDocument: "index.html", publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.DESTROY }) //Create Certificate const siteCertificateArn = new acm.DnsValidatedCertificate(this, "SiteCertificate", { domainName: WEB_APP_DOMAIN, hostedZone: zone, region: "us-east-1" //standard for acm certs }).certificateArn; } } 
Enter fullscreen mode Exit fullscreen mode

Note: CDK will automatically create an CNAME Record in Route53 for domain/subdomain dns validation. If you are using an external registrar, e.g godaddy.com to manage your DNS entries, the cdk deployment process will wait for you to manually add the DNS CNAME record and will continue after the the validation is check is complete.

6. Create CloudFront Distribution

 import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as route53 from '@aws-cdk/aws-route53'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWS_ACCOUNT_ID>", region: "<AWS_REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); //Create S3 Bucket for our website const siteBucket = new s3.Bucket(this, "SiteBucket", { bucketName: WEB_APP_DOMAIN, websiteIndexDocument: "index.html", publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.DESTROY }) //Create Certificate const siteCertificateArn = new acm.DnsValidatedCertificate(this, "SiteCertificate", { domainName: WEB_APP_DOMAIN, hostedZone: zone, region: "us-east-1" //standard for acm certs }).certificateArn; //Create CloudFront Distribution const siteDistribution = new cloudfront.CloudFrontWebDistribution(this, "SiteDistribution", { aliasConfiguration: { acmCertRef: siteCertificateArn, names: [WEB_APP_DOMAIN], securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019 }, originConfigs: [{ customOriginSource: { domainName: siteBucket.bucketWebsiteDomainName, originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY }, behaviors: [{ isDefaultBehavior: true }] }] }); } } 
Enter fullscreen mode Exit fullscreen mode

The new CloudFront distribution will use the newly created certificate. The deployment process will wait until the CloudFront instance is fully deployed before finishing. This could be a while.

7. Add Route53 A Record for react app to target Cloudfront distribution

 import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as route53 from '@aws-cdk/aws-route53'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as targets from '@aws-cdk/aws-route53-targets'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWS_ACCOUNT_ID>", region: "<AWS_REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); //Create S3 Bucket for our website const siteBucket = new s3.Bucket(this, "SiteBucket", { bucketName: WEB_APP_DOMAIN, websiteIndexDocument: "index.html", publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.DESTROY }) //Create Certificate const siteCertificateArn = new acm.DnsValidatedCertificate(this, "SiteCertificate", { domainName: WEB_APP_DOMAIN, hostedZone: zone, region: "us-east-1" //standard for acm certs }).certificateArn; //Create CloudFront Distribution const siteDistribution = new cloudfront.CloudFrontWebDistribution(this, "SiteDistribution", { aliasConfiguration: { acmCertRef: siteCertificateArn, names: [WEB_APP_DOMAIN], securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019 }, originConfigs: [{ customOriginSource: { domainName: siteBucket.bucketWebsiteDomainName, originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY }, behaviors: [{ isDefaultBehavior: true }] }] }); //Create A Record Custom Domain to CloudFront CDN new route53.ARecord(this, "SiteRecord", { recordName: WEB_APP_DOMAIN, target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(siteDistribution)), zone }); } } 
Enter fullscreen mode Exit fullscreen mode

Before we deploy the application let's first build. Create-React-App automatically builds the app to the build folder.

$>npm run build 
Enter fullscreen mode Exit fullscreen mode

8. Deploy react app

Our final CDK Infra script:

 import * as cdk from '@aws-cdk/core'; import * as s3 from '@aws-cdk/aws-s3'; import * as route53 from '@aws-cdk/aws-route53'; import * as acm from '@aws-cdk/aws-certificatemanager'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as targets from '@aws-cdk/aws-route53-targets'; import * as deploy from '@aws-cdk/aws-s3-deployment'; const WEB_APP_DOMAIN = "reactapp.nanosoft.co.za" export class Stack extends cdk.Stack { constructor(scope: cdk.Construct, id: string) { super(scope, id, { env: { account: "<AWS_ACCOUNT_ID>", region: "<AWS_REGION>" } }); //Get The Hosted Zone const zone = route53.HostedZone.fromLookup(this, "Zone", { domainName: "nanosoft.co.za", }); //Create S3 Bucket for our website const siteBucket = new s3.Bucket(this, "SiteBucket", { bucketName: WEB_APP_DOMAIN, websiteIndexDocument: "index.html", publicReadAccess: true, removalPolicy: cdk.RemovalPolicy.DESTROY }) //Create Certificate const siteCertificateArn = new acm.DnsValidatedCertificate(this, "SiteCertificate", { domainName: WEB_APP_DOMAIN, hostedZone: zone, region: "us-east-1" //standard for acm certs }).certificateArn; //Create CloudFront Distribution const siteDistribution = new cloudfront.CloudFrontWebDistribution(this, "SiteDistribution", { aliasConfiguration: { acmCertRef: siteCertificateArn, names: [WEB_APP_DOMAIN], securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2019 }, originConfigs: [{ customOriginSource: { domainName: siteBucket.bucketWebsiteDomainName, originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY }, behaviors: [{ isDefaultBehavior: true }] }] }); //Create A Record Custom Domain to CloudFront CDN new route53.ARecord(this, "SiteRecord", { recordName: WEB_APP_DOMAIN, target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(siteDistribution)), zone }); //Deploy site to s3 new deploy.BucketDeployment(this, "Deployment", { sources: [deploy.Source.asset("./build")], destinationBucket: siteBucket, distribution: siteDistribution, distributionPaths: ["/*"] }); } } 
Enter fullscreen mode Exit fullscreen mode

Run deployment one more time!

Note: if you're using an external registrar you need to add another CNAME record for your custom domain to point to the CloudFront Distribution

Screenshot 2021-07-30 at 12.17.18

You'll be able to navigate to the app custom domain.

Done!

Top comments (2)

Collapse
 
connor_hawley profile image
Connor Hawley • Edited

A note on the site distribution - the aliasConfiguration has been deprecated. Instead, use viewerCertificate. it will look something like this:

viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(siteCertificate, { aliases: [WEB_APP_DOMAIN], securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, sslMethod: cloudfront.SSLMethod.SNI }), 
Enter fullscreen mode Exit fullscreen mode

to do this you also need to make the return value of the siteCertificate not siteCertificateArn but the certificate object itself

Collapse
 
shadid12 profile image
Shadid Haque

awesome post