AWS in Plain English

New AWS, Cloud, and DevOps content every day. Follow to join our 3.5M+ monthly readers.

Follow publication

Deploy React Web App on AWS S3 and CloudFront using CloudFormation (via AWS SAM CLI)

What are we going to achieve?

In this article, we will walk through a demo project that uses CloudFormation to create a minimum set of cloud infrastructure resources on AWS to host a React web app.

The following diagram illustrates the major components:

That is, we will store static files (HTML, JavaScript, CSS, etc.) on an S3 bucket and serve these files via CloudFront (HTTPS) with caching.

What are the tools?

CloudFormation

We want to embrace the concept of “infrastructure as code”, which should make the cloud infrastructure setup reliable and repeatable. AWS provides CloudFormation as an official way to create and maintain a group of cloud resrouces as a “stack”.

CloudFormation allows us to write infrastructure code using a declarative and easy-to-read manner. It manages dependencies among resources very well, supports rollback out-of-the-box, and offers many other benefits.

AWS SAM

SAM stands for Serverless Application Model, which also comes from AWS. It consists of an extension of CloudFormation and a command-line tool. While it is designed for serverless applications, it can actually be used to manage and deploy everything CloudFormation supports.

We choose SAM here because the CLI is very convinient, and this choice can support us going further if we want to do more than this minimum setup (e.g., create the API server).

Create React App & TypeScript

They are the frontend tools we choose to get our minimum web app up and running.

Show me the code

The first step is to create the “template.yaml” file which should hold all our infrastructure code:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: >
SAM Template for React app hosted with S3 and CloudFront

Resources:
# Our cloud resources will be defined here ...

Outputs:
# Resource values we'd like to output for convient access ...

Then let’s take a look at the cloud resources we need to create:

  • AWS::S3::Bucket
  • AWS::S3::BucketPolicy
  • AWS::CloudFront::Distribution
  • AWS::CloudFront::OriginAccessControl

AWS::S3::Bucket

WebAppS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "${AWS::StackName}-web-app-s3-bucket"
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true

The config above shows how we configure the S3 bucket:

  • The logic id of the resource is WebAppS3Bucket
  • Bucket name is prefixed with a “stack name” to make it as unique as possible, while AWS::StackName is a pseudo parameter provided by CloudFormation
  • Public access to this bucket is blocked — the contents in this S3 bucket are supposed to be accessed by CloudFront only

AWS::S3::BucketPolicy

WebAppS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref WebAppS3Bucket
PolicyDocument:
Id: PolicyForCloudFrontPrivateContent
Version: "2012-10-17"
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub "arn:aws:s3:::${AWS::StackName}-web-app-s3-bucket/*"
Condition:
StringEquals:
AWS:SourceArn: !Sub arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}

The code above is the bucket policy configuration required to allow CloudFront to access the Bucket, and it comes from the AWS official guide:

  • The logic id of the resource is WebAppS3BucketPolicy
  • The policy allows a given CloudFront distribution to read the S3 bucket, while AWS::AccountId is a pseudo parameter provided by CloudFormation and CloudFrontDistribution is the logic id defined in the following code

AWS::CloudFront::Distribution

CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !GetAtt WebAppS3Bucket.RegionalDomainName
Id: !Sub "${AWS::StackName}-web-app-s3-origin"
OriginAccessControlId: !GetAtt WebAppOriginAccessControl.Id
S3OriginConfig: {}
Enabled: "true"
DefaultRootObject: index.html
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
- OPTIONS
TargetOriginId: !Sub "${AWS::StackName}-web-app-s3-origin"
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # CachingOptimized
OriginRequestPolicyId: 88a5eaf4-2fd4-4709-b370-b4c650ea3fcf # CORS-S3Origin
ViewerProtocolPolicy: redirect-to-https
PriceClass: PriceClass_All
CustomErrorResponses:
- ErrorCode: 404
ResponseCode: 200
ResponsePagePath: /index.html
- ErrorCode: 403
ResponseCode: 200
ResponsePagePath: /index.html

Basically:

  • The logic id is CloudFrontDistribution
  • It refers to WebAppS3Bucket we defined earlier when configuring the “origin”
  • It referes to WebAppOriginAccessControl we will define later when configure the access to origin
  • It uses AWS managed cache policy and origin policy

There is one thing requires special care here — CustomErrorResponse: We need to configure two rules that rewire 404 and 403 errors to a 200 success response with index.html.

The reason behind this config is that when using client-side routing solutions such as react-router, CloudFront should give control back to React instead of throwing errors.

For example, when user directly visiting a path such as /about, CloudFront will forward the request to fetch an object named about from the S3 bucket and then returns a 404 or 403 (because the object won’t exist in S3 bucket). However, in a single-page web application powered by React, the true intention of this direct visit is to let React to render content conditionally matching path /about.

Screenshot: error page when visit “/about” directly
Network inspection: error page when visit “/about” directly

AWS::CloudFront::OriginAccessControl

WebAppOriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub "${AWS::StackName}-web-app-origin-access-control"
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4

The last piece to configure is origin access control for CloudFront. This is a standard setting inferred by the AWS official guide.

Please note that many tutorials were setting origin access identity (OAI) instead of origin access control (OAC), which is a legacy setup that AWS no longer recommends.

Outputs

Outputs:
WebAppDomain:
Value: !GetAtt CloudFrontDistribution.DomainName
CloudFrontDistributionId:
Value: !GetAtt CloudFrontDistribution.Id
WebAppS3BucketName:
Value: !Ref WebAppS3Bucket

The last bit is to add some “outputs” so that we can easily read the values from AWS console and from a CI pipeline.

Example of viewing outputs in AWS console
# Example of getting CloudFront distribution id
aws cloudformation describe-stacks \
--stack-name thor-spike-react-s3-cloudfront \
--no-paginate \
--no-cli-pager \
--output text \
--query "Stacks[0].Outputs[?OutputKey=='CloudFrontDistributionId'].OutputValue"

All in one file

How do we deploy?

Deploy cloud infrastructure

First, build the template by running sam build:

Second, deploy to AWS.

If it is the first time to deploy manually, it is recommend to use guided deploy:

sam deploy --guided

It will ask a bunch of questions and generate samconfig.toml file

If it is supposed to run in CI/CD pipeline, then we can provide samconfig.toml file directly and run sam deploy without guide.

Deploy React web app

We need to build and bundle the app first via npm run build , this command should generate the bundled files under build folder.

Then we can run the deploy script, which does two things:

  • Sync (copy) the bundled files to S3 bucket
  • Invalidate CloudFront cache

Code repo

The full code for infrastructure and web app can be found on my Github: https://github.com/zzdjk6/thor-react-s3-cloudfront-sam

More content at PlainEnglish.io.

Sign up for our free weekly newsletter. Follow us on Twitter, LinkedIn, YouTube, and Discord.

Interested in scaling your software startup? Check out Circuit.

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Published in AWS in Plain English

New AWS, Cloud, and DevOps content every day. Follow to join our 3.5M+ monthly readers.

Written by Thor Chen

Passionate JavaScript/TypeScript Developer with a Full-stack Background

Responses (1)

Write a response