jonelantha: Blog


How to setup a Gatsby site on AWS using GitHub Actions - Part 2: CloudFront index Lambda@Edge function

29th February 2020

This is Part 2 of a how-to guide to help you host your Gatsby site on AWS (with Continuous Deployment provided by GitHub Actions). See Part 1 for more information.

NOTE: This information should be useful for anyone looking to add Apache style index.html handling to a CloudFront distribution (regardless of whether you're using Gatsby or not)

Out of the box CloudFront has very basic handling of index.html pages. We need to add the following behaviour otherwise some Gatsby generated links won't work:

  1. When a user requests a page at /my-blog-post (a directory without a trailing /) we need CloudFront to respond with a 301 response to redirect the user's browser to /my-blog-post/ (the same directory with a trailing slash /)
  2. When a user requests a page at /my-blog-post/ (a directory with a trailing /) we need to serve them the index.html page within that directory: /my-blog-post/index.html.

Lambda@Edge to the rescue

Lambda@Edge allows us to add some logic to CloudFront to modify how CloudFront responds to user requests. We'll a add a small block of code to implement the missing functionality mentioned above.

Creating the Lambda

  1. In the AWS console go to the Lambda section
  2. Lambda@Edge functions need to be built in the North Virginia region. Click the region in the top right menu and select US East (N. Virginia) us-east-1
  3. On the main Lambda dashboard, click Create function
  4. Select Author From Scratch
  5. In the Basic information section:

    • Function Name: Enter CloudFrontIndexHandler
    • Runtime: leave as Node.js 12.x
  6. In the Permissions section:

    • Expand Choose or create an execution role

      • Execution role: Select Create a new role from AWS policy templates
      • Role Name: Enter CloudFrontIndexHandler-role
      • Policy templates: start typing CloudFront and then select Basic Lambda@Edge permissions (for CloudFront trigger) when you see it in the list
  7. Click Create Function
  8. Cut and paste the following code into the code window:

    const { extname } = require('path');
    
    exports.handler = async function(event) {
        const request = event.Records[0].cf.request;
    
        const requestPath = request.uri;
    
        if (looksLikeADirectory(requestPath) && !requestPath.endsWith('/')) {
            const pathWithSlash = `${requestPath}/`;
    
            return makeRedirect301Response(pathWithSlash, request.querystring);
        }
    
        if (requestPath.endsWith('/')) {
            const pathWithIndexHtml = `${requestPath}index.html`;
    
            return makeOriginRequestWithNewPath(request, pathWithIndexHtml);
        }
        
        return request;
    };
    
    function makeRedirect301Response(path, queryString) {
        const redirectUri = combineUriParts(path, queryString);
    
        return {
            body: '',
            status: '301',
            statusDescription: 'Moved Permanently',
            headers: {
                location: [
                    {
                        value: redirectUri,
                    },
                ],
            },
        };
    }
    
    function looksLikeADirectory(path) {
        const fileExtension = extname(path);
    
        return fileExtension === '';
    }
    
    function makeOriginRequestWithNewPath(request, path) {
        return {
            ...request,
            uri: path,
        };
    }
    
    function combineUriParts(path, queryString) {
        return queryString
            ? `${path}?${queryString}`
            : path;
    }
  9. Click Save (orange button top-right)
  10. Click the Actions button (a menu should appear)
  11. Click Deploy To Lambda@Edge (if you don't see this option it's probably because you're not in the N.Virginia region, see the second step above)
  12. In the Deploy to Lambda@Edge window:

    • Distribution: if this box has something in it, click the x to clear it. On the list which appears click the ID for your CloudFront distribution (see Part 1 if you need to determine the ID of your CloudFront distribution)
    • Cache Behavior: Leave as *
    • CloudFront event: leave as Origin Request
    • Include Body: leave unticked
    • Confirm deploy to Lambda@Edge: tick the confirmation checkbox
    • Click Deploy
    • Close the window

If you're interested to see how the Lambda function was linked to the CloudFront distribution you can do the following:

  • In the AWS console go to the CloudFront section

    • Select the appropriate Distribution in the list
    • Click the Behaviours tab
    • Tick the single checkbox in the behavious table and then click Edit
    • Scroll down and you should see the link to the Lambda

Also we created an IAM role which gives the Lambda limited permissions to access various other AWS resources. If you're interested in reviewing the newly created role, do the following:

  • Go to the IAM section of the console

    • Click Roles
    • Click CloudFrontIndexHandler-role in the list
    • Click the Access Advisor tab and you should see the role grants permission to create CloudWatch log entries

So how does this work?

The code above will be run each time CloudFront tries to fetch a file from the S3 bucket (the Origin of the CloudFront distribution). The code checks the requested url and it either responds with a 301 redirect or it tells CloudFront to fetch the appropriate index.html file from the S3 bucket.

And how much will this cost?

Unfortunately there's no free tier for Lambda@Edge functions, however pricing is low. CloudFront only executes the Lambda when it needs to fetch a file from S3 and due to caching that won't happen very often. So costs for using this Lambda should be negligable (if anything).

OK Lambda created. What's next?

Next we need to setup the Continuous Deployment environment using GitHub Actions.

Let's move on to Part 3: Continuous Deployment with GitHub Actions.


© 2003-2020 jonelantha