In this post, I am going to demonstrate how you can deploy a Static Site or a pure Single Page Application to Amazon S3 and serve it through CloudFront and Lambda@Edge functions. We will also see how we can setup a Continuous Deployment Pipeline.
Pros of using pure S3 Static Site Hosting
- S3, by default, adds
index.html
to the URLs. So,www.example.com
becomeswww.example.com/index.html
andwww.example.com/blog
becomeswww.example.com/blog/index.html
. But, CloudFront only does that for the root URL. So,www.example.com
does becomeswww.example.com/index.html
butwww.example.com/blog
stayswww.example.com/blog
. - S3: Redirects as metadata.
/path1.html
-> 301:/path2.html
/path3.html
-> 301:www.example.com
However, S3 redirects are ignored by CloudFront./path1.html
->/path1.html
/path3.html
->/path3.html
Cons of using pure S3 Static Site Hosting
- High latency as no caching at edge locations
- S3 reads are very expensive than CloudFront reads (count of requests).
- Difficult to manage custom domains
So, this is the reason why is better than just .
Let’s get started
Create a S3 bucket
- Go to AWS console and select S3
- Create a new bucket
You can configure the bucket however you like, but the 3 main things that are must-configure things here are the name of the bucket, the location, and Check the “Block all public access” checkbox. We only want to allow access to our content/site in S3 through CloudFront because that is cheap and fast and we can configure many other things here.
- After creating, select your bucket by clicking on its name
Configuring CloudFront
- Go to AWS console and select CloudFront
- Click on
Create Distribution
- Select the
Get Started
of the web distribution
- Select the previously created bucket as Origin Domain Name
- For Origin Path, if your content is not in the root directory of your bucket, add the path. In our case, we are going to use the entire bucket for just a single site and the site will be in the root directory. So, we will leave this field blank.
- Set
Restrict Bucket Access
to Yes.
- Select
Create a new identity
- Select
Yes, Update Bucket policy
forGrant Read Permissions on Bucket
. This will add a new policy in our bucket’s policies that will allow CloudFront to read from the bucket. We do this, because we turned off public access to our bucket.
- Scroll down below to
Default Cache Behaviour Settings
- Set
Viewer Protocol Policy
toRedirect HTTP to HTTPS
- For
Cache and Origin Request Settings
, selectUse legacy cache settings
Leave the cache time setting untouched. We are going to set these through the command line.
- Select
Yes
forCompress Objects Automatically
- Scroll down to
Distribution Settings
- You can configure your custom domain name like the following (if you don’t want to, then just leave them blank and at defaults):
It is not in the scope of this post to create a custom domain and certificates and assigning it to CloudFront. If you want to, then you will have to look in the docs (it is quite straight forward). In future, I might create a post for this too.
- Scroll down a bit and set the
Default root object
toindex.html
- Now, just click the
Create Distribution
button to create your distribution. It might take a while.
Configure the Bucket Policy
We need to do this because if we try to visit a route that doesn’t exist in S3, then it will throw a instead of because CloudFront doesn’t know which files exist in our S3 bucket. So, we provide CloudFront with a ListBucket
Policy, so that CloudFront can see what is there in the bucket and return if something doesn’t exist there.
- Go to your bucket’s policy settings tab.
- Click
Edit
and create a copy of the first statement and make the following changes and save.
Configuring Path Redirects
Our application, may or may not have the path we are trying to visit. So, we would want to return 404.html
for them. Gatsby
generates a 404.html
file for us and we need to set that as a response for paths. Other frameworks might have different files (check their docs). For this post, I am deploying a pure React SPA. So, I need to send index.html
as a response for all paths. To configure all of this, do the following:
- Go to your Distributions configuration page and select the
Error Pages
tab.
- Click the
Create Custom Error Response
button. - You can customize it however you like. If a path is not found, I am sending back the
/index.html
page and the new Response Status will be (it could be anything you want).
Fixing the bug .html bug
If in the previous step, you set 200 as new response, then you probably don’t need to do this. But, if you are using a static site instead of a pure Single Page Application, then you probably set 404 as the new response too, even if you are passing the correct 404 page path. You would do this if you want to send 404 as a response if a page doesn’t exist on your site instead of 200. One problem arises here is that, if you try to visit www.example.com/blog
, then CloudFront won’t automatically append .html
to the end of the path. Because of this, CloudFront will throw a 404 error. The blog page will be rendered on your screen, but if you check the network tab, you will see a 404 code and it was because your /404.html
had javascript in it that knew how to handle /blog
path. The users won’t notice any problem but the search engine crawlers rely on the response’s status code to crawl a page. This might hamper your SEO. Here is how you can fix it:
- Go to the AWS console and select lambda.
- Select the us-east-1 region because Lambda@ functions only work if you select that region. Don’t worry, you will still be able to access this all over the world.
- Click
Create Function
- Give any Name to the function. I choose
index-doc-fixer
. - Scroll down and expand
Change default execution role
and selectCreate a new role from AWS policy templates
. - Give a role name and select the
Basic Lambda@Edge permissions
template.
- Click
Create Function
. - Add the following function code, and click
Deploy
:
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
if (uri.endsWith("/")) {
request.uri += "index.html";
} else if (!uri.includes(".")) {
request.uri += "/index.html";
}
return request;
};
- Click the
Add Trigger
Button at the top. - Then, for trigger, select
CloudFront
and then clickDeploy to Lambda@Edge
. - In the form, select your distribution.
- Set Cache behaviour to
*
. - Set
CloudFront
event toOrigin Request
. - Check the acknowledge box and hit
deploy
.
Redirect Manager
Now, if we want to redirect to a different page/site on visit of a specific page (configured in S3), let us say /redirect
. Now, when I visit /redirect
, in the network tab you will see that S3 did send a redirect-header but the problem is that CloudFront doesn’t know what to do with that header. So, it just adds that header to the response and responds to the client. To make the redirection work:
- Create a new function named
redirect-manager
just like we did above in the previous section and add the following function-code.
- Click
Deploy
and then clickAdd Trigger
Button at the top. - Then, for trigger, select
CloudFront
and then clickDeploy to Lambda@Edge
. - In the form, select your distribution.
- Set Cache behaviour to
*
. - Set
CloudFront
event toOrigin Response
. - Check the acknowledge box and hit
deploy
.
Acquiring Credentials
- Go to Create New User
- Give any name and under
Access Type
only selectProgramatic Access
.
- Next, in the Permissions, select the
Attach Existing Policies Directly
tab and chooseAmazonS3FullAccess
andCloudFrontFullAccess
. - Next, in the Tags section, just click Next.
- Your review screen should look something like this
- Click
Create User
. - You will see credentials named,
Access Key ID
andSecret Access Key
. - Go to your Github Repository of the application and then click
Settings > Secrets
. There, create 2 secrets namedAWS_ACCESS_KEY_ID
andAWS_SECRET_ACCESS_KEY
. The names has to be the same. Set their values to the values of the credentials.
Setting up continuous deployment pipeline
Now that we have configured S3, CloudFront and Lambda@Edge functions, we can now proceed to setup the pipeline to deploy our website to S3 and invalidate the CloudFront cache so that next time our users visit our site, we deploy the latest artefacts instead of the previous cached site. To do this, we are going to use the Github Actions.
- In your repository, create a directory:
.github
and inside that create another directoryworkflows
. Now, create a file.github/workflows/build-and-deploy.yml
. To configure Actions, we use a yaml file. - To the YAML file, add the following content.
name: Deploy to AWS
on:
push:
branches:
- master
# This will only run when there is a new commit on the master branch
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checking out code
uses: actions/checkout@v1
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-south-1
- name: Install Dependencies
run: npm install
- name: Build
run: npm build
- name: Deploy static site to S3 bucket
run: aws s3 sync ./build/ s3://auth.vighnesh153.com --delete --cache-control 'public, max-age=300, s-maxage=31536000'
# aws s3 sync ./build/ s3://<YOUR-BUCKET-NAME> --delete --cache-control 'public, max-age=300, s-maxage=31536000'
# public in cache-control means that anyone can cache our content
# max-age is for browser cache (300 seconds -> 5 minutes)
# s-maxage is for intermediate proxies, in this case, CloudFront CDN. (31536000 seconds -> 1 year).
# As we are going to invalidate the caches as soon as we upload a new build,
# there is no harm in caching our site in CDN for a year. You can even choose more time if you like.
- name: Cloudfront invalidate
run: aws cloudfront create-invalidation --paths '/*' --distribution-id E1QD0AN1LK04E4
# aws cloudfront create-invalidation --paths '/*' --distribution-id <YOUR-CLOUDFRONT-DISTRIBUTION-ID>
There you go. Now, whenever you push commits to the master branch, it will trigger this pipeline and deploy your code the S3 and invalidate the CloudFront caches automatically. That is Continuous Deployment for you all.
Conclusion
This was a huge post. I hope this helped you in setting up automatic deployments of your frontend apps to AWS.
Although, if you have just a couple of apps, then this approach is fine. But if you have multiple apps then doing this initial setup will be quite repetitive. For that, you can use CloudFormation’s templates to reduce some work. Not going to discuss CloudFormation here because it is out of scope of this post. In future, I might write something about it.
If you liked this, drop a comment below to show your love and support. Thanks.
Thanks for publishing such great information. You are doing such a great job. This information is very helpful for everyone. Keep sharing about Cloud Service Provider. Thanks.
ReplyDeleteThanks for publishing such great information. You are doing such a great job. This information is very helpful for everyone. Keep sharing about Empresas De Hosting En Colombia. Thanks.
ReplyDelete