Deploy your static React app to AWS Cloudfront using CDK
Paul Allies
Posted on July 30, 2021
Here we illustrate how to do the following
- Create React App
- Setup CDK
- Get the AWS Hosted Zone
- Create S3 bucket for react app
- Create Certificate
- Create Cloudfront Distribution with certificate
- Add Route53 A Record for react app to target Cloudfront distribution
- Deploy react app
1. Create React App
$> npx create-react-app reactapp.nanosoft.co.za
To test, cd into the application folder and run
$> npm start
You should see the following appear
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
and the following dev dependency:
npm i -D @types/node \
typescript \
source-map-support
Within the same application root folder, create a cdk folder and a cdk.json file and here we'll write our infrastructure code.
Within the cdk folder create 2 files:
├── cdk
│ ├── index.ts
│ └── stack.ts
├── cdk.json
and add the following to the cdk.json file
{
"app": "node cdk/index.js"
}
//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>"
}
});
}
}
//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');
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"
now run the deploy script
$> npm run deploy
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
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);
}
}
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
})
}
}
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;
}
}
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
}]
}]
});
}
}
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
});
}
}
Before we deploy the application let's first build. Create-React-App automatically builds the app to the build folder.
$>npm run build
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: ["/*"]
});
}
}
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
You'll be able to navigate to the app custom domain.
Done!
Posted on July 30, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.