Deploy Static Sites and pure SPAs to AWS using S3, CloudFront and Lambda@Edge functions

Deploy Static Sites and pure SPAs to AWS using S3, CloudFront and Lambda@Edge functions

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, becomes and becomes But, CloudFront only does that for the root URL. So, does becomes but stays
  • S3: Redirects as metadata.
    • /path1.html -> 301: /path2.html
    • /path3.html -> 301:
      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 CloudFront+S3CloudFront + S3 is better than just S3S3.

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 for Grant 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 to Redirect HTTP to HTTPS
  • For Cache and Origin Request Settings, select Use legacy cache settings

Leave the cache time setting untouched. We are going to set these through the command line.

  • Select Yes for Compress 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 to index.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 403403 instead of 404404 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 404404 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 200200 (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, 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@EdgeEdge 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 select Create 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 click Deploy to Lambda@Edge.
  • In the form, select your distribution.
  • Set Cache behaviour to *.
  • Set CloudFront event to Origin 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 click Add Trigger Button at the top.
  • Then, for trigger, select CloudFront and then click Deploy to Lambda@Edge.
  • In the form, select your distribution.
  • Set Cache behaviour to *.
  • Set CloudFront event to Origin Response.
  • Check the acknowledge box and hit deploy.
Acquiring Credentials
  • Go to Create New User
  • Give any name and under Access Type only select Programatic Access.
  • Next, in the Permissions, select the Attach Existing Policies Directly tab and choose AmazonS3FullAccess and CloudFrontFullAccess.
  • 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 and Secret Access Key.
  • Go to your Github Repository of the application and then click Settings > Secrets. There, create 2 secrets named AWS_ACCESS_KEY_ID and AWS_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 directory workflows. 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  
      - master  
# This will only run when there is a new commit on the master branch
    runs-on: ubuntu-latest  
      - name: Checking out code  
        uses: actions/checkout@v1  
      - name: Configure AWS Credentials  
        uses: aws-actions/configure-aws-credentials@v1  
          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:// --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.


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.


  1. 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.

  2. Thanks 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.


Post a Comment