Launching A Static Site With React & AWS CloudFront

After months of dragging my feet and contemplating ideas I finally got around to redesigning my personal website. Visually, it’s not winning any awards. It’s simple and to the point and I wanted to keep the back-end just as simple. I’ve used services like DigitalOcean and Heroku in the past to host websites for clients and just found it a pain to setup load balancing and perform general server maintenance. Those tasks don’t have to be difficult and usually aren’t but I’d rather spend my time actually developing software for clients.

AWS as an alternative offers static website hosting through a combination of S3 and their CloudFront CDN service. I’ve personally found the freedom from concerns about scale and server maintenance alone to be worth the switch. The speed increases from having the site served through a CDN and cost savings don’t hurt either. Read more about Cloudfront here.

In this article we will:

  • Generate a static website using Create React App
  • Configure an AWS IAM user to manage our website on AWS
  • Use an AWS CloudFormation template to generate our AWS CloudFront stack
  • Deploy the website using the AWS CLI

Before beginning you should have:

  • A beginner’s understanding of React, Create React App ,and
  • AWS resources (CloudFormation, CloudFront, IAM, S3). I will not cover these in great detail as the documentation available is quite extensive.

This example is available here on github.


The Website

For the sake of this article, we won’t be doing anything other than creating an app using Create React App and running

yarn run build

If you have an existing website you would like to use, ensure it is static (does not run a server like Express). Once you have your app built you are ready to proceed.

AWS IAM configuration

As an IAM best practice, it’s important we not use our root account to access AWS. Learn more here. If you do not already have an IAM user outside of your root account, create one following the AWS user guide here.

When your IAM user has been created, make a note of the User ARN as we’ll be using it in our CloudFormation template to give this user permission to manage our resources. Your user should atleast have permission to manage S3, CloudFormation, and CloudFront resources.

The CloudFormation template

AWS CloudFormation is great. It provides us a simple way to manage a collection of Amazon resources from a single location. A great benefit is that we can add it to Git and track how our stack has changed over time, making it easier to return to a stable configuration in the event you make a serious error in the future.

In the interest of brevity, I’ve included my CloudFormation template below. You’ll have to make some modifications to get it to work for you. When deployed, it will create 2 S3 buckets, 2 S3 bucket policies, and the CloudFront distribution resource.

AWSTemplateFormatVersion: 2010-09-09
Resources:
  myDistribution:
    Type: 'AWS::CloudFront::Distribution'
    Properties:
      DistributionConfig:
        Origins:
          - DomainName: mys3bucket-example.s3.amazonaws.com
            Id: S3Origin
            S3OriginConfig:
              OriginAccessIdentity: ''
        Enabled: 'true'
        DefaultRootObject: index.html
        Logging:
          IncludeCookies: 'false'
          Bucket: mys3logbucket-example.s3.amazonaws.com
          Prefix: myprefix
        DefaultCacheBehavior:
          AllowedMethods:
            - GET
            - HEAD
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: 'false'
            Cookies:
              Forward: none
          ViewerProtocolPolicy: allow-all
        PriceClass: PriceClass_100
        ViewerCertificate:
          CloudFrontDefaultCertificate: 'true'
    DependsOn:
      - MyStaticSiteBucket
      - MyLogBucket
  MyStaticSiteBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: mys3bucket-example
      WebsiteConfiguration:
        IndexDocument: index.html
  MyLogBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: mys3logbucket-example
    DependsOn:
      - MyStaticSiteBucket
  MyStaticSiteBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref MyStaticSiteBucket
      PolicyDocument:
        Statement:
          - Sid: ReadAccess
            Action:
              - "s3:GetObject"
            Effect: "Allow"
            Resource: 'arn:aws:s3:::mys3bucket-example/*'
            Principal: "*"
          - Sid: ListWriteDeleteAccess
            Action:
              - "s3:ListBucket"
              - "s3:PutObject"
              - "s3:DeleteObject"
            Effect: "Allow"
            Resource:
              - 'arn:aws:s3:::mys3bucket-example/*'
              - 'arn:aws:s3:::mys3bucket-example'
            Principal:
                AWS: 'arn:aws:iam::example:user/myuser'
  MyLogBucketPolicy:
    Type: 'AWS::S3::BucketPolicy'
    Properties:
      Bucket: !Ref MyLogBucket
      PolicyDocument:
        Statement:
          - Sid: ListReadWriteDeleteAccess
            Action:
              - "s3:ListBucket"
              - "s3:GetObject"
              - "s3:PutObject"
              - "s3:DeleteObject"
            Effect: "Allow"
            Resource:
              - 'arn:aws:s3:::mys3logbucket-example/*'
              - 'arn:aws:s3:::mys3logbucket-example'
            Principal:
                AWS: 'arn:aws:iam::example:user/myuser'

mys3bucket-example – This is the name of the S3 bucket that will contain our website code and function as the store CloudFront. It needs to be a unique name across all existing buckets currently on S3. More can be learned here.

mys3logbucket-example – The name of the S3 bucket that will hold logs generated from our CloudFront stack. Follows the same naming conventions as our other S3 bucket.

example:user/myuser – The IAM user resource name from the Amazon IAM console. Links our user the bucket access policies

AWS CLI

With the template ready, we now need to configure our development environment to connect to AWS. Follow the instructions here to install AWS CLI if you haven’t already and run

aws configure

to link your AWS account. Use your access key and secret key values from the .csv file you downloaded earlier. You can leave the defaults for the region name and output format fields.

Now we’re ready to create our stack! Run the next command from the project’s root directory.

aws cloudformation deploy --stack-name my-cloud-stack --template-file myTemplate.yaml

You should now see CloudFormation building our stack in the AWS console. The deploy command is nice because it will automatically create the stack for us if it doesn’t already exist, and if it does it will perform an update. A more convenient approach to calling the create and update stack commands explicitly. Learn more about deploy here.

When it has completed, run

// "build" is the desired directory to upload
aws s3 sync build s3://mys3bucket-example  --delete

substituting “mys3bucket-example” with your S3 bucket name to upload our build directory to our website store. The --delete option will remove files that are not in the directory we’re uploading.

Note: build specifies the directory to upload to S3. If your production directory is different ie. dist, the command will need to be updated accordingly.

With the completion of the upload you can now navigate to your CloudFront domain name shown in the screenshot or the S3 url and view your page online!

Next Steps

Use a custom domain with your CloudFront hosted website – Limit access to your website so it is only accessible through CloudFront (disable access through the S3 address). Attach a domain name through the Route 53 service and modifying the stack CNAMES.

Error Handling – One of the caveats of serving a React app through a static site is because there is only one entry point (index.html), if visitors try to navigate to another page like /about or /contact they’ll be greeted with an ugly 404 page. One way to address this is to set an Error Document in the S3 bucket properties page, but this only addresses one error status. Another way is to direct all responses to index.html and handle the errors client side, this isn’t the greatest approach however. It doesn’t make the greatest sense to handle a 502 error client side. Defining custom error responses through CloudFront in the AWS console is probably the best solution.