If at first, you don’t succeed, try, try, again

I’m back! It’s been literally years since my last blog post and I’ve been buried in work, and home life – oh, and of course that whole pandemic thing going on.

Got married? Check.
Had a child? Check.
Did a whole bunch of DevOps stuff I can block about? Big ol’ check.

CloudFront announced last spring the availability of CloudFront Functions, and I recently had a project that gave us a good opportunity to stretch the legs of the service. As with all new services, sometimes the documentation doesn’t quite align with reality, things are missing from the tutorials, or there are just some weird quirks you really only find out about when you get deep into implementation. I’m going to share with you my lessons learned and hopefully a few tidbits to help you better understand the service for your implementation.

Tl;dr? Are you a Typescript shop, want an easy-to-use project boilerplate for CloudFront Functions, AND have it be CI/CD ready? Go grab it! https://github.com/skr-labs/cloudfront-function-ts

Alright, let’s jump into it!

My specific use case around CloudFront Functions was we wanted users to be able to opt-in to using a new version of our web app which was hosted at a different subdomain. As our app is behind a login flow, we wanted to be able to store a user’s choice to use the new app, and redirect them once they opted-in before they had to log in. This saved users from being required to log in twice, and also allowed us to redirect long before the Angular app had time to load (which in some cases can be a few seconds). In order to make this work, we leveraged our existing integration with LaunchDarkly in the application to target specific users who were eligible to switch to the new app, then had the app set a cookie indicating the user had opted-in to the switch.

With all that back-end application logic implemented, we looked to using CloudFront Functions to achieve the redirect for a few reasons:

  1. Speed. CloudFront Functions run and must complete in less than 1ms.
  2. Price. Versus Lambda@Edge which we’ve used extensively it has a free tier (2m invocations), and the per request pricing is $0.10/million invocations vs $0.60/million + GB/s used of Lambda@Edge.
  3. Simplicity. We can have a relatively simple function put right into our CloudFormation templates, no need for all the overhead of creating, managing, and publishing Lambda functions.

Knowing your use case and how you plan to achieve it will be critical to evaluating if CloudFront Functions are right for you. There are some very big drawbacks depending on what you need to achieve:

  1. Limited to a pure Javascript (ECMAScript 5.1 compliant) environment, so no Node.js, no Python, Go, etc.
  2. You can’t touch origin request or response events, viewer request and response only
  3. You’re limited to 1ms of execution time, 2MB of memory, and less than 10KB of function size
  4. There’s no network, filesystem, or request body access. The network access is a big one, obviously there’s a lot of overhead to performing network calls and rightfully so it’s ommitted from CloudFront Functions, but it completely eliminates the ability for functions to be dynamic and behave differently per execution based on outside data (like in S3, or DynamoDB).

For our use case, all we needed to do was to detect that the cookie existed in the request, and perform the appropriate magic for redirection. CloudFront Functions ended up being a perfect place to do that.

In the interest of brevity of this blog post, I won’t go into every minute detail of my implementation, but I will touch quickly on a few pain points, and what we did about them.

Pain Point 1 – Testability

The documentation is pretty good for a service launch, but it’s very limited to traditional Javascript, which works for the service ECMAScript 5.1 limitation, but makes it difficult for me to interpret as a Typescript superset guy. In order to test my function, I need to export it. As soon as I add the export const testable = handler; directive I was getting runtime errors in CloudFront. It turns out that I rely too heavily on the expectation that my execution environment has the commonjs module available, as indicated in my tsconfig.json – the line in my transpiled output exports.testable = void 0; was throwing an exports is undefined at runtime.

In the sample repository, I worked around this by manipulating the transpiled Javascript after it’s outputted with the scripts/postbuild.js script. I cheated a bit, and because the use strict directive is inherent in the environment, I just did a quick little .replace().

Pain Point 2 – Size Limitation

My function wasn’t particularly large, but I wanted to ensure I wasn’t pushing my CloudFormation template sizes either (there’s a maximum allowed template size). So when I add the function to the template, even at 10KB worth of text, that’s a lot to add – and can also make the template pretty unwieldy with stringified Javascript code right inline.

This is where the minification was a nice addition and something I included in thescripts/postbuild.js. Simply put, it does an EMCAScript 5 compatibile minification of the transpiled Javascript and stringifies the whole thing so you can just copy/paste it right into your template.

The sample repository reduces the file from 1,374 bytes to 740 bytes, which is a 34% reduction. The more complex your function, the more required minification will be.

Once everything is said and done, if you wanted to pipeline the project, you could simply add the build/src/index.min.js as a build artifact, and have your build script load and pipe it into your CloudFormation template on the fly prior to deployment.

Pain Point 3 – Maintaining Querystring Parameters on Redirect

Okay in all fairness this isn’t really a CloudFront Functions problem, but it is something that was missing from the example – and something we needed for our Angular app use case.

The biggest issue here is that CloudFront gives you the query string parameters in an object notation, and we want to verbatim include them in our URI. I’ve included in the sample code a fairly straightforward (and dependency-free) way in how we implemented maintaining the query string parameters as part of the redirected URL, which basically just converts them back into a string (one less thing for you to tackle).

Wrapping Up

All said and done, CloudFront Functions worked spectacularly for our use case – and came together fairly painlessly (once we figured out unit testing).

I hope the example repository gives you a great starting point!



, , , ,