Deploy your static React app to AWS Cloudfront using CDK
Tags: aws, website, cloudfront, react
Published: 2021-07-30 10:00:25

Here we illustrate how to do the following
1. Create React App
1. Setup CDK
1. Get the AWS Hosted Zone
1. Create S3 bucket for react app 
1. Create Certificate
1. Create Cloudfront Distribution with certificate
1. Add Route53 A Record for react app to target Cloudfront distribution
1. Deploy react app


### 1. Create React App

```
$> npx create-react-app reactapp.nanosoft.co.za

```
To test, cd into the application folder and run
```bash
$> npm start
```
You should see the following appear

![Screenshot 2021-07-30 at 09.54.17](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5n4sqc7a8ks7pmyg8u1n.png)
 
### 2. Setup CDK
To deploy our application to AWS using CDK,  we need to install the following dependencies:

```bash
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:

```bash
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.


![Screenshot 2021-07-30 at 11.58.40](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/spzf9rq435rmtb0mkcv6.png)

Within the cdk folder create 2 files: 

```
├── cdk
│   ├── index.ts
│   └── stack.ts
├── cdk.json

```
 
and add the following to the cdk.json file

 ```json

 {
    "app": "node cdk/index.js"
}
 ```

```ts
//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>"
            }
        });

    }
}

```

```ts
//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:

```json
    "cdk-build": "tsc --target ES2018 --moduleResolution node --module commonjs cdk/index.ts",
    "deploy": "npm run cdk_build && cdk deploy"
```

now run the deploy script

```bash
$> 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


```ts
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


```ts

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

```ts

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
```ts

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

```ts

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.

```bash
$>npm run build
```
### 8. Deploy react app
Our final CDK Infra script:

```ts

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


![Screenshot 2021-07-30 at 12.17.18](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qr49p5h47vbggopcgmyr.png)

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

Done!