How to Deploy an AWS S3 Hosted React SPA through CloudFront

Introduction

The purpose of this short tutorial is to demonstrate how to deploy an AWS S3 hosted React Application using Cloud Front. 
To successfully complete this tutorial you will need:
  • Linux Bash shell
  • React framework 
  • Serverless framework
  • Text editor
  • AWS Free tier account

Step 1: Creating a React Application

For the purposes of this tutorial a simple react application will be built using the automatic application generation tools.
The following command can be run to create a react application in the current directory:
jafrimo@LP00650:~/tutorials$ npx create-react-app aws-cloudfront-react-app

To validate this has completed successfully, you can change into the newly created application  directory aws-cloudfront-react-app and start up the react application locally (we're not in AWS yet!) as follows:
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ npm start

If all goes well, then depending on your OS setup you may see the following window come up in your browser. If not, you can manually open the React app by navigating to http://localhost:3000/


We now have the code for a working React Application that we are ready to build and deploy to a production environment.

Step 3: Generate a production-optimised build of the React Application

The application we have locally run at this point is not yet form suitable for public distribution through CloudFront. To achieve this,. we can run the following command:
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ npm run build
> aws-cloudfront-react-app@0.1.0 build /home/jafrimo/tutorials/aws-cloudfront-react-app
> react-scripts build
Creating an optimized production build...
Compiled successfully.

What we get at the end of a successful build is a "build" folder that (in theory) is as simple to deploy as copying over to a web server's public HTML folder.
This folder may look something like this:

.
├── asset-manifest.json
├── favicon.ico
├── index.html
├── manifest.json
├── precache-manifest.054774adbe886ee6e3c29227ef1745b5.js
├── service-worker.js
└── static
    ├── css
    │   ├── main.2cce8147.chunk.css
    │   └── main.2cce8147.chunk.css.map
    ├── js
    │   ├── 2.b41502e9.chunk.js
    │   ├── 2.b41502e9.chunk.js.map
    │   ├── main.28647029.chunk.js
    │   ├── main.28647029.chunk.js.map
    │   ├── runtime~main.a8a9905a.js
    │   └── runtime~main.a8a9905a.js.map
    └── media
        └── logo.5d5d9eef.svg
We are now ready to write some infrastructure automation scripts for deploying this folder to the AWS cloud!

Step 4:  YAML File for Deployment to AWS

The Serverless Framework provides a layer of abstraction over cloud-vendor specific infrastructure automation tools, eg, CloudFormation in the case of AWS.
This section describes how to write a single Serverless Framework YAML script that will achieve the following goals:
1) Creating an AWS S3 Bucket that will host our React Application build files
2) Creating an AWS CloudFront distribution for making the React Application accessible to the public web through Amazon's content delivery network
3) Restrict the S3 Bucket to only be accessible to the public through the CloudFront distribution
3) Automatically deploying the React production optimised build to the AWS S3 bucket from the local machine
Create a file called serverless.yaml in text editor in the root of the react application folder. This is "aws-cloudfront-react-app" in our example.
This file consists of the following sections, described individually. The entire serverless.yaml file can be found on the GitHub page.

Service Name, Provider, Plugins, and Custom

The following lines show the initial part of the YAML file. The indentation in YAML files is essential to proper parsing, so if copying and pasting this, please make sure that the spaces are exactly as given here:
  • The Service Name describes the overall infrastructure stack - this can be anything you like
  • The provider for this tutorial is AWS, and the region I'm deploying into is EU-WEST-2. The runtime section is not relevant to our tutorial but still has to be present. Although the Serverless Framework supports other cloud providers, this tutorial will only work on AWS due to the specific Amazon resource dependencies such as S3.
  • The plugins section includes an entry for a plugin that will allow automatic deployment of the production build to an S3 bucket. 
  • The custom section defines the name of the S3 bucket we want to create - remember this has to be unique across the entire set of all S3 buckets, so you may need to adjust this variable
  • The custom section includes a directive telling the plugin that the build folder (which contains our production optimised react build) needs to be uploaded to S3
service: aws-cloudfront-react-app
provider:
  name: aws
  runtime: nodejs6.10
  region: eu-west-2
plugins:
  - serverless-s3-sync
custom:
  siteName: aws-cloudfront-react-app
  s3Sync:
    - bucketName: ${self:custom.siteName}
      localDir: build

S3 Bucket

Next, we have the Resources section which includes AWS-specific Resource configurations.  The first of these is an S3 Bucket definition. The bucket is defined as publicly readable, and some instruction is given to AWS to treat this bucket as a website, rather than just a file object store. The ${XXXXXX} syntax enables the serverless framework to cross reference any constants declared in the custom section. If the referenced bucket name is already taken, an alternative can be declared in the siteName constant .

resources:
  Resources:
    #=========================================================================
    # S3 Bucket for hosting the app
    WebSiteBucket:
      Type: AWS::S3::Bucket
      Properties:
        AccessControl: PublicRead
        BucketName: ${self:custom.siteName}
        WebsiteConfiguration:
          IndexDocument: index.html
    #=========================================================================


CloudFront Origin Access Identity

This section puts a layer of security over the bucket read access, insuring that it's only ever accessed through the CloudFront network. A special type of identity is a created for this purpose and a Bucket Policy is applied which allows the CloudFront original access identity to call S3 Get Object.
#=========================================================================
#
# CLOUD FRONT IDENTITY THAT WILL READ FROM BUCKET
CloudFrontOriginAccessIdentity:
  Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
  Properties:
    CloudFrontOriginAccessIdentityConfig:
      Comment: aws-cloudfront-react-app-OAI
#=========================================================================
#
# ONLY ALLOW CLOUD FRONT TO READ THE BUCKET
BucketPolicyCloudFrontRead:
  Type: 'AWS::S3::BucketPolicy'
  Properties:
    Bucket: !Ref WebSiteBucket
    PolicyDocument:
      Statement:
      - Principal:
          CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
        Action: 's3:GetObject'
        Effect: Allow
        Resource:
          Fn::Join:
            ''
            -
              - Fn::GetAtt: WebSiteBucket.Arn
              '/*'
#=========================================================================

CloudFront Distribution

The following portion is the setup of a CloudFront content distribution network (CDN) in front of an Origin Server, which in our example is the S3 bucket created earlier. The CDN ensures that consumers load the React application as fast as possible by 1) delivering it through their nearest edge location and 2) caching it for future requests. The caching time-to-live (TTL) settings are specified in seconds, so the DefaultTTL setting means that any new features or bugfixes on the React App would not show up for the user until a minute - this is a trade-off from caching.
Although it's not relevant for S3 origins, the MaxTTL setting ensures that irrespective of HTTP cache headers on the origin response, resources are not cached for any longer than 24hrs.

#=========================================================================
#
# CLOUD FRONT DISTRO FOR THE S3 APP
ReactAppDistribution:
  Type: AWS::CloudFront::Distribution
  Properties:
    DistributionConfig:
      Origins:
      - DomainName:
          !Sub '${self:custom.siteName}.s3.amazonaws.com'
        Id: s3Origin
        S3OriginConfig:
          OriginAccessIdentity: !Join [ "", [ "origin-access-identity/cloudfront/", !Ref CloudFrontOriginAccessIdentity ] ]
      Enabled: 'true'
      Comment: The bucket hosting a react app
      DefaultRootObject: index.html
      DefaultCacheBehavior:
        DefaultTTL : 60
        MaxTTL : 86400
        MinTTL : 0
        AllowedMethods:
        - DELETE
        - GET
        - HEAD
        - OPTIONS
        - PATCH
        - POST
        - PUT
        TargetOriginId: s3Origin
        ForwardedValues:
          QueryString: 'false'
          Cookies:
            Forward: none
        ViewerProtocolPolicy: allow-all
      PriceClass: PriceClass_200
      ViewerCertificate:
        CloudFrontDefaultCertificate: 'true'
#=========================================================================

Step 5: Generate AWS Stack & Deploy SPA using the Serverless Framework

Before this YAML script can be executed through the Serverless framework deployment process, the serverless-s3-sync plugin needs to be installed. Serverless framework plugins are installed on a per-service basis, so the following command has to be run within the "aws-cloudfront-react-app" folder:
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ npm install --save serverless-s3-sync

Once this has been done, you can run the deployment into AWS as follows. This process will take a long time (~15 minutes) when run for the first time, and whenever any configuration settings are changed on the CloudFront distribution. Presumably, this is because the CloudFront resource deployments require global distribution to all of Amazon's CDN edge locations. 

jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ sls deploy --force --verbose
Serverless: Packaging service...
Serverless: Uploading CloudFormation file to S3...
Serverless: Uploading artifacts...
Serverless: Validating template...
Serverless: Updating Stack...
Service Information
service: aws-cloudfront-react-app
stage: dev
region: eu-west-2
stack: aws-cloudfront-react-app-dev
resources: 5
api keys:
None
endpoints:
None
functions:
None
layers:
None
Stack Outputs
ReactAppDistributionOutput: d3o2e8g5rtoidi.cloudfront.net
ServerlessDeploymentBucketName: aws-cloudfront-react-app-serverlessdeploymentbuck-k7pzdbyvk1zr
S3 Sync: Syncing directories and S3 prefixes...
.
S3 Sync: Synced.

Step 6: Validate publicly accessible web application

If all has worked, you should be able to browse to the URL shown under "ReactAppDistributionOutput" to view the website, .eg,, https://d3o2e8g5rtoidi.cloudfront.net. from the example given here. The output should unsurprisingly be the same as Step1 

Step 7: Make a Change in React Application Code and Republish

We can now start changing our application to shape into whatever we want. Lets' simply change line of code from src/App.js as follows:
src/App.js
...
        <p>
          This is a React App deloyed to AWS S3
        </p>
...
The production optimised build needs to be re-done to reflect this now
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ npm run build
> aws-cloudfront-react-app@0.1.0 build /home/jafrimo/tutorials/aws-cloudfront-react-app
> react-scripts build
Creating an optimized production build...
Compiled successfully.

After doing this we rredploy our application. Notice how, this time, there is no re-instantiation of either the S3 bucket or the CloudFront distribution. The only action that will happen is the production optimised build being uploaded to the S3 bucket. 
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ sls deploy --verbose

We can open a browser window against the same domain once again to validate our change has gotten deployed . Remember to wait at least ONE MINUTE before doing this, of course, as our Cloud Front caching TTL aws set to 60s.



Step 8 : Clean up

To ensure you don't get billed for unwanted resources, it's usually a good idea to tidy up AWS resources. This is especially important if opening up any public website as we have done, through which someone could potentially flood the site with thousands of unwanted requests. Where a traditional web hosting server might max out and crash, this "serverless" combination of CloudFront and S3 will happily take whatever you throw at it and scale up elastically with a £££ bill. 
The following command will clean up all AWS resources created in this tutorial. Once again, this step will take a long time (~15 mins) as this action has a global scope.
jafrimo@LP00650:~/tutorials/aws-cloudfront-react-app$ sls remove
S3 Sync: Removing S3 objects...
..
S3 Sync: Removed.
Serverless: Getting all objects in S3 bucket...
Serverless: Removing objects in S3 bucket...
Serverless: Removing Stack...
Serverless: Checking Stack removal progress...
.........
Serverless: Stack removal finished...



Comments

Popular posts from this blog

AWS Glue to ingest a REST API into a Relational Database

Evolution of Data Engineering over the Last 10 Years