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
For now, we can just enter our bucket name and leave everything as default
Enable static hosting
Here, we will enable hosting which is present under the Properties
tab
Allowing Public access
Now, let's go to the Permissions
tab and edit the bucket settings to allow public access
Scrolling down, we will also update our bucket policy to allow s3:GetObject
to Principal *
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/*"
]
}
]
}
Perfect, now let's build our react app
yarn build
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
Nice! now we should be able to access our website through the 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
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
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.
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
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
Introduction to Infrastructure as Code with Terraform
Karan Pratap Singh ・ Aug 17 '21
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!
Top comments (3)
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 :)
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.
Hi Jack
Please find a proper solution for this:
itisoktoask.me/cloudfront-redirect...
I had the same issue (with that very page :)) - the source of the issue explained in the blogpost (tl;dr: redirect to index.html is done by default by any web server. but neither s3 or cloudfront ARE web servers :)) My solution uses official AWS solution (CloudFront function) but my post actually explains the steps :)) I committed this tutorial to the official repo, but still waiting for review (probably indefinitely, doesn't look like AWS is bothered to monitor their repos).
I hope the tutorial helps
Grendel