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