Published on

Deploy React app to S3 & Cloudfront

8 min read

Authors
banner

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
├── 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

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/*"]
    }
  ]
}

s3 policy-update

Perfect, now let's build our react app

yarn build

build and upload

And sync the build with our myapp.com S3 bucket

aws s3 sync build s3://myapp.com

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/*"
    }
  ]
}

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
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
chmod +x ./scripts/deploy.sh

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

./scripts/deploy.sh "YOUR_BUCKET_NAME" "YOUR_DISTRIBUTION_ID"

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
}

Let's tf apply and see the magic!

$ tf apply

...

Outputs:

website_cdn_id = "ABCDXYZ"
website_endpoint = "abcdxyz.cloudfront.net"

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!

© 2024 Karan Pratap Singh