This post is intended to share the process of migrating URL prettifying functionality for a static site hosted in AWS to Cloudfront Functions from Lambda@Edge and deploying them with Serverless.

A bit of background

This website is built using a static site generator called Hugo, deployed using the Serverless Framework and hosted in AWS using S3 for storage, Cloudfront as a CDN and up until recently Lambda@Edge. At build time all content is rendered into static HTML and CSS files, there is no client side magic that something like React provides. Hugo produces an index.html for every post in it’s own folder, I was using Lambda@Edge to transform more friendly paths like /some-fancy-post-name into /some-fancy-post-name/index.html that S3 understands so that the content can actually be found. It is nothing fancy, just appending index.html to any path that doesn’t have it.

What are Cloudfront Functions?

Cloudfront Functions, whilst not a successor to Lambda@Edge, are an evolution of them. Whereas Lambda@Edge allows fully fledged lambda functions to run within a regional edge cache (AWS region) closest to the request origin, Cloudfront Functions run right at the edge where the content is ideally going to be served from. In terms of Cloudfront architecture you cannot get closer to the user. There are some restrictions to Cloudfront Functions you have to bear in mind like having a limited runtime, execution time, memory allowance, package size and that they are javascript only with no network access.

What is the Serverless Framework?

The Serverless Framework is an infrastructure-as-code tool that simplifies the defining and deployment of mostly event driven serverless architectures. When used with AWS it is an abstraction over Cloudformation and does a lot of the heavy lifting to create and wire up resources like lambdas, queues, apis and events. As it compiles down to Cloudformation it can support other resources too but these are defined in regular Cloudformation syntax.

Migrating pretty URLs from Lambda@Edge to Cloudfront Functions

Migrating the Lambda@Edge that handled pretty URLs to a Cloudfront Function was pretty straight forward. The code itself went from this:

exports.handler = (event, \_, callback) => {
  const request = event.Records[0].cf.request;
  const indexPath = new RegExp("/$");
  const match = indexPath.exec(request.uri);
  const newURL = request.uri;

  if (match) {
    newURL = `${request.uri}index.html`;
  }

  request.uri = newURL;
  callback(null, request);
};

to this:

function handler(event) {
  var request = event.request;
  var indexPath = new RegExp("/$");
  var match = indexPath.exec(request.uri);
  var newURL = request.uri;

  if (match) {
    newURL = `${request.uri}index.html`;
  }

  request.uri = newURL;
  return request;
}

As you can see very little changed, sourcing the value for request was the only functional change to the code required. Some other changes were required because of the restricted runtime for Cloudfront Functions:

  • No exports allowed!
  • No let or const, not used good old fashioned hoistable var in a while

The feature availability in the Cloudfront Function runtime is a real jumble of pre-ES5 plus a bit of ES6, ES7 and beyond. And no exports! I was keen to unit test these functions and the lack of exports presented a challenge, I will write up how I went about that in a future post.

Cloudfront Functions code is also required to be inline in the CloudFormation template. This isn’t great from a testability and IDE syntax linting/hinting/etc points of view. Also, only one function per trigger is allowed which makes reusability of the code a challenge. For example with my development version of the site I expected to be able to use two Functions in a pipeline on the viewer-request trigger, one for HTTP Basic Auth and one for pretty URLs, then only pretty URLs in production. Alas, the code had to be duplicated, and this limitation already exists with Lambda@Edge.

For the inline code problem, I was fortunate enough to be using a Typescript file to define my Serverless config instead of yml so I just read an external file containing the full code for my Cloudfront Function:

fs.readFileSync("./lambdas/cfFunctions/prettyUrls.js", "utf8")

This worked like a charm but is there an alternative? Yes, use CDK, which handles reading the code in from a file for you (docs).

Summary

Migrating from Lambda@Edge to Cloudfront Functions was an opportunity to try something new from AWS and push some compute as close to the end user as possible. It went really well and the bulk of the time I spent doing it was spent figuring out unit testing (I wrote about that here) and a clean deployment strategy as opposed to writing the code and just getting it deployed.

For simple uses that manipulate the request/response objects without the need for external input then Cloudfront Functions are a better alternative to Lambda@Edge, otherwise Lambda@Edge isn’t going anywhere. For those simple use cases migrating between the two is pretty straight forward though deployment and a robust development lifecycle for Cloudfront Functions can have a little bit more complexity.

Get in contact

If you have comments, questions or better ways to do anything that I have discussed in this post then please get in contact.