I mentioned in an earlier post about my intention to learn more about the AWS eco-system, as I plan to take a more hands-on approach to my personal DevOps needs. I am pleased to say that I am super happy with the learning progress I have made in the past few weeks.

TL;DR

To make changes to my site, I open a pull-request to the main branch on GitHub. I can preview the branch changes using Gatsby Cloud before merging. To complete the process, I merge the branch into main, and the changes are automatically deployed to my Amazon S3 bucket and distributed to all 216 points of presence (PoPs) on the AWS CloudFront network.

Now I have an almost perfect Lighthouse score from any location and a fast, highly secure, highly available site.

My site runs under the www subdomain, and I wrote a 301 serverless redirect function to route all traffic there, which is the perfect scenario for a solid SEO strategy.

During this process, I gained exposure to Gatsby Cloud for building my Gatsby site and the following AWS services for hosting it.

  • Amazon Identity and Access Management (Security)
  • Amazon S3 Storage Buckets (Storage)
  • Amazon CloudFront Distributions (CDN)
  • Amazon Certificate Manager (SSL)
  • Amazon Route 53 (DNS)
  • Amazon Lambda@Edge (Serverless)

Hosting a Gatsby site on AWS S3 and AWS CloudFront (CDN)

The rest of this post will cover the steps I took. Feel free to copy all or parts of this process depending on your needs. I have included a table of contents below for quick access.

If you plan to copy this process, please be aware that perhaps not all of the steps are needed for your specific project. This guide assumes you already have a Gatsby project and it is versioned in GitHub.

Building, Deploying and Hosting a Gatsby Site

There are multiple options to consider when hosting a Gatsby site.

I took reference from the following pages on the gatsbyjs.org site, but I feel like these guides are a little short on the details in some places, so I made this a step-by-step guide.

You may want to swap out various sections of this guide with your own preferences. Perhaps you prefer to use a different process to build your site or have a more complex 301 redirect strategy. I have tried to separate the content into logical sections, but because of the nature of a build system, this is not easy. I have tried to explain when and why I have done things to make this guide more useful.

Step 1: Set up Gatsby Cloud

Gatsby Cloud is free for personal projects and single-purpose sites. If you only plan to build a single repository you can continue to use it for free with up to 100 real-time edits per month, the free tier will probably work for most personal sites.

If you plan to host several sites, you should consider the first pay tier on Gatsby Cloud is currently $19 per month which would enable 250 real-time edits per month.

Another point to note is that Gatsby Cloud currently only works with GitHub hosted repositories. They plan to add more soon (GitLab, Bitbucket, Google).

Connect GitHub to Gatsby Cloud

  • Go to https://www.gatsbyjs.com.
  • Sign up with your GitHub account.
  • Click Add a site +.
  • Select the Organization, Repository and Branch you wish to build.
  • Skip or Add Integrations.
  • Click Create site.

Your site will start building to the Gatsby staging servers, assuming the build is successful you will now be able to view it on a Gatsby staging URL (eg: https://build-random-string.gtsb.io).

To learn more about Gatsby Cloud and Data Source (CMS) Integrations check out the Gatsby Cloud Docs.

Adding password protection to the Gatsby staging environment

A neat feature of Gatsby Cloud is the preview environment, you can add password protection to keep the staging environment private. It could be great for showing changes to clients before pushing them live.

  • Go to Site Settings > General > Access Control.
  • Click Edit.
  • Choose Password Protected.
  • Enter a password in the Preview Password field.

Now your Gatsby Cloud site staging URL will be password protected.

Step 2: Set up an Amazon S3 bucket

We will need to do several things to prepare an S3 bucket for Gatsby Cloud to deploy too. We will create a new IAM User and a new S3 bucket.

Gatsby Cloud will need the following bits of information from AWS. An Access Key ID, Secret Access Key and an S3 Bucket Name.

Create a new IAM User

In the AWS Management Console go to the Identity and Access Management (IAM) service:

  • Create a new IAM User called GatsbyCloudAmazonS3FullAccess.
  • Give the user Programmatic access.
  • For the user permissions choose to manually Attach existing policies directly and search for and select AmazonS3FullAccess.

Follow the wizard and make a note of the Access key ID and Secret access key when you are finished.

Create a new S3 bucket

In the AWS Management Console go to the Amazon S3 service:

  • Create a new S3 bucket with a unique name.
  • Tag the bucket to track costs.
  • Allow public access by un-checking Block all public access.
  • Check Block public access to buckets and objects granted through new access control lists (ACLs).
  • Check Block public access to buckets and objects granted through any access control lists (ACLs).

Enable the S3 bucket for static website hosting

After creating the bucket we need to enable the Static website hosting feature.

  • Click on the newly created bucket name.
  • Go to the Properties Tab.
  • Then enable Static website hosting and choose Use this bucket to host a website.
  • Enter index.html for the Index document.
  • Enter 404.html for the Error document.
  • Save your changes and make a note of the endpoint, you can use this address to test your site after your first deployment. If you visit the URL now, you should see a 403 Forbidden page

You can review this document for additional information about hosting static sites on Amazon S3.

Set permissions on the new S3 bucket

To enable public access to the S3 bucket we need to change the bucket policy.

  • Go to the Permissions Tab.
  • Ensure you have cleared Block all public access.
  • Under Bucket policy enter the following configuration JSON and exchange example-name under the Resource section to be the unique name of your S3 bucket:
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PublicReadGetObject",
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject"],
      "Resource": ["arn:aws:s3:::example-name/*"]
    }
  ]
}

After setting the above policy, if you revisit the S3 bucket endpoint URL, you should now see a 404 Not Found page.

You can review this document for additional information about Setting permissions for website access.

Step 3: Connect Gatsby Cloud to Amazon S3

Now we can go back to the Gatsby Cloud platform and connect the build to the newly created S3 bucket.

  • Go to Site Settings > Integrations > Hosting.
  • Locate AWS S3 Integration in the list of Hosting integrations and click Connect.
  • Enter your IAM Access Key ID.
  • Enter your IAM Secret Access Key.
  • Enter your S3 Bucket Name.

After pressing save, it will trigger a new deployment. If everything goes as expected, you should wait a few moments for the deployment to complete. Once complete you can now visit your S3 URL endpoint from earlier, and you should now see your Gatsby site deployed on the AWS bucket endpoint address.

Step 4: Set up Amazon CloudFront CDN

You will probably want also to set up AWS CloudFront, which is Amazon's content delivery network (CDN). There are two reasons for this:

The first reason is that S3 buckets can't be configured to serve content over HTTPS to custom domains because Amazon S3 uses an SSL wildcard certificate.

The second reason is that using a CDN to distribute your static assets means your site speed will be impressive from most countries, not just countries close to where you hosted the new S3 bucket. (The CloudFront network has 216 points of presence (PoPs)).

Create a new SSL certificate for your domain name

In the AWS Management Console go to the AWS Certificate Manager (ACM) service:

  • Click on Request a certificate.
  • Enter your Domain name, I entered michaelfasani.com and I entered an Additional name as www.michaelfasani.com (I read some people seem to be having various issues with HTTPS in this kind of S3 setup and I believe the problem is from not specifying the www address as an additional name. You should add all the subdomains you want to work with).
  • Then choose to validate the certificate with either DNS validation or Email validation and follow the next steps for either choice.

Create a new CloudFront distribution

In the AWS Management Console go to the Amazon CloudFront service:

  • Click on Create Distribution.
  • Choose Web and click Get Started.
  • Paste your S3 domain URL in the Origin Domain Name field, do not select the bucket from the list.
  • Under Default Cache Behavior Settings > Viewer Protocol Policy select Redirect HTTP to HTTPS.
  • Under Default Cache Behavior Settings > Allowed HTTP Methods select GET, HEAD, OPTIONS.
  • Under Cache and origin request settings select Use legacy cache settings (This is needed for our 301 redirect serverless function later).
  • Under Distribution Settings > Alternate Domain Names (CNAMEs) enter your domain name (eg: www.michaelfasani.com).
  • Under Distribution Settings > SSL Certificate choose Custom SSL Certificate and select the certificate we created earlier.
  • Click Create Distribution.
  • After a few minutes, once the status changes to Deployed, you should now be able to view your site on the ********.cloudfront.com*** domain name listed in the CloudFront Distributions service space.

You can review this document for additional information, How do I use CloudFront to serve a static website hosted on Amazon S3?.

Step 5: Attaching a custom domain name to the CloudFront distribution

WARNING! There are several approaches to this step. I have become increasingly displeased with my current hosting provider. I have used my existing hosting company for more than 15 years. Unfortunately, they were acquired last year by a new company, and I have since had several issues.

I plan to move the domain name registration to AWS eventually, but I only just paid for an additional year. For now, I have left the domain name with my current provider but will use Amazon Route 53 as the DNS service. You may wish to move your name to AWS or just point the nameservers to AWS (which is what I did for now).

This process can be different for you, but ultimately you need to point your domain name to the CloudFront distribution.

My domain currently does not get much traffic, and I also was not worried if the domain was offline for several days. If keeping your website online is essential, then be very careful with these next steps.

Set up Amazon Route 53 to be the DNS service for my domain name

WARNING! This step may not be for you. I have a very basic set of records on my domain name. I do not use mail or any additional services with my domain. I only point my name to this website, and I do not use any subdomains. I used this guide Making Route 53 the DNS service for an inactive domain. There is also another guide for active domains if you want to avoid potential downtime.

In the AWS Management Console go to the AWS Certificate Manager (ACM) service:

  • Go to DNS management > Create hosted zone
  • Enter your domain name without www and click Create hosted zone.
  • The next steps you need to do twice, once for an A record and once for a AAAA record.
    • Click Create record and select Simple routing.
    • Click Define simple record.
    • Under Record name put www.
    • Under Value/Route traffic to select Alias to a CloudFront distribution.
    • Under Choose Distribution paste your CloudFront domain name. (My name was not appearing in the list of possible selections).
    • Under Record type select an A record type.
    • Repeat the process above for AAAA record type.
  • Now we need to create two apex domain alias records (An apex domain is without a subdomain, for example, www).
  • The next steps you need to do twice, once for an A record and once for a AAAA record.
    • Click Create record and select Simple routing.
    • Click Define simple record.
    • Leave the Record name blank.
    • Under Value/Route traffic to select Alias to another record in this hosted zone.
    • Under Choose record select your previously created A record (eg: www.example.com).
    • Under Record type select an A record type.
    • Repeat the process above for AAAA record type.

The apex domain record (example.com) now points to the www record (www.example.com). The www record points to the CloudFront distribution.

The alias record is not the same as a 301 redirect. We will create a 301 redirect later, but for that to work, we need to resolve the apex domain. The 301 redirect we create happens during a request to the CloudFront distribution, that is the reason why we need both the apex and www records pointing to CloudFront location.

Change the nameservers for your domain name to Amazon's nameservers

Warning! This process can take up to 48 hours to complete.

This next step will be different for you, depending on where you registered your domain name. Where you purchased your name, you will need to change the nameservers to point to Amazon's servers. Under the Hosted zone details section where you see the newly created A and AAAA records you will also see an NS record. These four addresses are the nameservers you will need to use with your domain name registrar.

Step 6: Redirecting non-www traffic to www using a Lambda@Edge serverless function

This final step is the icing on the cake. It seems people have been struggling with 301 redirects when using CloudFront plus S3 when using HTTPS.

For SEO reasons most sites choose to route all traffic to a single domain so that search engines only record one specific URL per piece of content.

On an Apache web server, you either create a .htaccess file (for directories representing separate sites on one server) or httpd.conf file (at the root of the Apache installation). This is the type of scripting logic we are recreating using AWS Lambda@Edge.

If you visit any of the URL variations below for my site you will get redirected to my secure www URL version.

Original AddressRedirect Address
https://www.michaelfasani.comhttps://www.michaelfasani.com
http://www.michaelfasani.comhttps://www.michaelfasani.com
https://michaelfasani.comhttps://www.michaelfasani.com
http://michaelfasani.comhttps://www.michaelfasani.com

I assume the thinking behind most of the tutorials out there perhaps predate the Lambda@Edge release date and the solutions on the web seem like hacked workarounds. People are, in most cases, making an empty S3 bucket or similar trick. I spent quite some time Googling this last part, trying to understand what was the best approach.

Create a 301 redirect Lambda@Edge serverless function

In the AWS Management Console go to the AWS Lambda service:

  • Click Create Function.
  • Choose to Author from scratch.
  • Enter 301-redirect as the Function name.
  • Runtime was default as Node.js 12.x.
  • For Execution role choose Create a new role with basic Lambda permissions.
  • Click Create Function.

At this point, you will not be able to deploy any Lambda function to CloudFront. If you carry on with writing a function eventually you end up with a slightly cryptic error when trying to publish the function:

Correct the errors above and try again.
Your function's execution role must be assumable by the edgelambda.amazonaws.com service principal.

Preparing the Lambda IAM role we just created

In the previous step, we allowed AWS to create a new IAM role for us. This role needs some additional changes to allow it to publish our function to CloudFront.

In the AWS Management Console, go to the Identity and Access Management (IAM) service:

  • In the left-hand navigation go to Access management > Roles.
  • Here you should see a new role which was just created. It will have a name similar to the function name we created (eg: 301-redirect-role-****).
  • Click on the Role name.
  • On the next screen, you will see it was assigned a policy AWSLambdaBasicExecutionRole.
  • We need to make this role assumable by the edgelambda.amazonaws.com service principal.
  • Click on the Trust Policies tab.
  • Click Edit Trust Relationship.
  • Modify the JSON object to include "edgelambda.amazonaws.com".
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
  • Click Update Trust Policy to save the changes.

Publish your Lambda function to CloudFront

Now we can start to configure and publish our 301 redirect serverless function. Paste the following 301 redirect code below in the Function code window and press Save.

// Return a 301 redirect response or the original request
exports.handler = async (event) => {
  const request = event.Records[0].cf.request;
  const host = request.headers.host[0].value;

  if (host === 'example.com') {
    const response = {
      status: '301',
      statusDescription: 'Moved Permanently',
      headers: {
        location: [
          {
            key: 'Location',
            value: 'https://www.example.com',
          },
        ],
        'cache-control': [
          {
            key: 'Cache-Control',
            value: 'max-age=3600',
          },
        ],
      },
    };

    return response;
  }

  return request;
};

Under the Designer section we need to:

  • Click on + Add Trigger.
  • Select CloudFront from the dropdown.
  • Click Deploy to Lambda@Edge.
  • Change the CloudFront event to Viewer Request.
  • Check Confirm deploy to Lambda@Edge.
  • Click Deploy.

Check the Behaviors on the CloudFront Distribution

In the AWS Management Console go to the Amazon CloudFront service:

  • Edit the newly created CloudFront Distribution.
  • Go to the Behaviors tab.
  • Select your S3 container and click Edit, scroll down to the bottom of the page and you should see your 301-redirect function under Lambda Function Associations.
  • It takes a while for the function to propagate across all edge locations, soon you should be able to test the 301 redirect by visiting your domain without the www subdomain prefix, and you should be redirected with a 301 to your www subdomain.

Handling more complex redirects

If your redirect needs are more complex or you just want additional information about Lambda@Edge redirects, check out the following AWS resources:

That's a Wrap!

Wow, well that was a pretty long and intense process. Initially, it took me several full days to put this build together and to solidify my understanding of this process. If you noticed any issues with the configuration here or if it helped you put together your perfect Gatsby CI/CD process, I would love to hear from you, drop me a comment below.