DEV Community

Cover image for Deploy React app to S3 & Cloudfront
Karan Pratap Singh
Karan Pratap Singh

Posted on

Deploy React app to S3 & Cloudfront

In this article, we will look at how we can deploy our webapp to AWS S3 with AWS Cloudfront as our CDN. We'll look at a simple way to automate our deployments as well.

As a bonus, we'll also see how we can use Terraform to manage our infrastructure in the long run!

Note: All the code is available in this repository

Project setup

I'll be using React app I've initialized using create react app (CRA) but this guide is valid for pretty much any framework!

yarn create react-app s3-cloudfront
Enter fullscreen mode Exit fullscreen mode
├── node_modules
├── public
│   ├── favicon.ico
│   ├── index.html
│   ├── manifest.json
│   └── robots.txt
├── src
│   ├── App.css
│   ├── App.js
│   ├── index.css
│   ├── index.js
│   └── logo.svg
├── package.json
└── yarn.lock
Enter fullscreen mode Exit fullscreen mode

Setup S3

Create Bucket
Let's create a new S3 bucket

s3 dashboard

For now, we can just enter our bucket name and leave everything as default

s3 create

s3 created

Enable static hosting

Here, we will enable hosting which is present under the Properties tab

s3 properties
s3 enable hosting
s3 hosting-done

Allowing Public access

Now, let's go to the Permissions tab and edit the bucket settings to allow public access

s3 enable-public
s3 public-enabled

Scrolling down, we will also update our bucket policy to allow s3:GetObject to Principal *

s3 policy

Here's the bucket policy json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::YOUR_S3_NAME/*"
            ]
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

s3 policy-update

Perfect, now let's build our react app

yarn build
Enter fullscreen mode Exit fullscreen mode

build and upload

And sync the build with our myapp.com S3 bucket

aws s3 sync build s3://myapp.com
Enter fullscreen mode Exit fullscreen mode

If you're new to using AWS CLI, feel free to checkout my other article on setting up the CLI from scratch_

Great! seems like our build was synced with our S3 bucket

s3 done

Nice! now we should be able to access our website through the bucket endpoint.

bucket endpoint

Note: You can view your bucket endpoint by re-visiting the static deployment section under the Properties tab

Cloudfront

Let's connect our Cloudfront with our S3 endpoint. If you're not familiar with Cloudfront, it's a content delivery network (CDN) that delivers our data (images, videos, API's, etc.) globally (based on customer's geographical location) at low latency, high transfer speeds.

Let's create a Cloudfront distribution
cf dashboard

You should be able to select your S3 endpoint directly from the dropdown.

We'll also create a new origin access identity (OAI) and allow CloudFront to update bucket policy

cf create

Cloudfront should automatically update your bucket policy by adding an additional principal as shown below.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity DISTRIBUTION_ID"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*"
        }
    ]
}
Enter fullscreen mode Exit fullscreen mode

For now, I'll be leaving most of the fields as default but you can configure ssl, logging, https redirection, and much more here.

cf create ok

cf done

After a few minutes, your distribution would be deployed and you should be able to access your content at distribution DNS!

Invalidation

When we re-deploy or sync our updated build we need to also create an invalidation rule which basically removes an object cache before it expires. This can be really important when serving updates to your web app

invalidation
invalidation create

Note: Here, we just invalidate * all objects for simplicity, but you might want to customize this depending on your use case

Automating deployments

Now let's automate our deployment process so that we can use it from our CI (eg. Github actions) on events like pull request merge etc.

Here's a simple deploy script that installs the dependencies, builds the app, syncs it with our S3 bucket, and then invalidates CloudFront distribution cache.

touch scripts/deploy.sh
Enter fullscreen mode Exit fullscreen mode
BUCKET_NAME=$1
DISTRIBUTION_ID=$2

echo "-- Install --"
# Install dependencies
yarn --production

echo "-- Build --"
# Build
yarn build

echo "-- Deploy --"
# Sync build with our S3 bucket
aws s3 sync build s3://$BUCKET_NAME
# Invalidate cache
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*" --no-cli-pager
Enter fullscreen mode Exit fullscreen mode
chmod +x ./scripts/deploy.sh
Enter fullscreen mode Exit fullscreen mode

Now, from our CI we can simply execute our script to create a deployment

./scripts/deploy.sh "YOUR_BUCKET_NAME" "YOUR_DISTRIBUTION_ID"
Enter fullscreen mode Exit fullscreen mode

Terraform (Bonus!)

Too many clicks? Let's setup our infrastructure using Terraform. If you're not familiar with Terraform, you can checkout my other article

Here's a sample terraform

provider "aws" {
  region = "us-east-1"
}

variable "bucket_name" {
  default = "myapp.com-sample"
}

resource "aws_s3_bucket_policy" "bucket_policy" {
  bucket = aws_s3_bucket.deploy_bucket.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.deploy_bucket.arn}/*"
      },
    ]
  })
}

resource "aws_s3_bucket" "deploy_bucket" {
  bucket = var.bucket_name
  acl    = "public-read"

  website {
    index_document = "index.html"
    error_document = "index.html"
  }
}

resource "aws_cloudfront_origin_access_identity" "cloudfront_oia" {
  comment = "example origin access identify"
}

resource "aws_cloudfront_distribution" "website_cdn" {
  enabled = true

  origin {
    origin_id   = "origin-bucket-${aws_s3_bucket.deploy_bucket.id}"
    domain_name = aws_s3_bucket.deploy_bucket.website_endpoint

    custom_origin_config {
      http_port              = "80"
      https_port             = "443"
      origin_protocol_policy = "http-only"
      origin_ssl_protocols   = ["TLSv1", "TLSv1.1", "TLSv1.2"]
    }
  }

  default_root_object = "index.html"

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD", "DELETE", "OPTIONS", "PATCH", "POST", "PUT"]
    cached_methods         = ["GET", "HEAD"]
    min_ttl                = "0"
    default_ttl            = "300"
    max_ttl                = "1200"
    target_origin_id       = "origin-bucket-${aws_s3_bucket.deploy_bucket.id}"
    viewer_protocol_policy = "redirect-to-https"
    compress               = true

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

output "website_cdn_id" {
  value = aws_cloudfront_distribution.website_cdn.id
}

output "website_endpoint" {
  value = aws_cloudfront_distribution.website_cdn.domain_name
}
Enter fullscreen mode Exit fullscreen mode

Let's tf apply and see the magic!

$ tf apply

...

Outputs:

website_cdn_id = "ABCDXYZ"
website_endpoint = "abcdxyz.cloudfront.net"
Enter fullscreen mode Exit fullscreen mode

Next Steps?

Now that we've deployed our static assets to S3 and using Cloudfront as our CDN. We can connect our distribution dns with Route 53 to serve it through our own domain.

Hope this was helpful, feel free to reach out to me Twitter if you face any issues. Have a great day!

Discussion (2)

Collapse
cleancodestudio profile image
Clean Code Studio

Great stuff @karanpratapsingh ! I'm an Amazon dev, and loved how you went through the steps in this article (along with the YouTube video).

Loved the YouTube video, and the fact that you didn't have a single filler word like "mmm" or "umm" is just down right impressive!

You've earned a new youtube subscriber and dev.to follower :)

Collapse
jh0 profile image
Jack • Edited

This is good but it doesn't cover routing to index.html for child routes of the single page web app. If you navigate directly to for example '34jg34.cloudfront.net/books' it won't resolve to index.html for the web app to resolve, it will try to find a resource called books in the S3 bucket. Anything that isn't in the bucket it should redirect to index.html. I couldn't get this to work so I've resorted to sharing the S3 bucket url itself.