Skip to content

Latest commit

 

History

History

edge-lambdas

🔋 Pleo SPA Infra - Lambda@Edge Lambdas

Edge Lambda functions that support serving of an SPA via CloudFront using cursor files.

Setup

The source code of the lambdas lives in the src directory. Each edge lambda's index.js file exposes a handler method which is the entrypoint used in CF configuration.

Test

Tests are collocated with the source files, one test suite per lambda. They are using Jest. Run tests with make test.

Build

Each lambda is built using Vercel's ncc which takes care of bundling and compiling. Run build of all lambdas with make build

Config

Since Lambda@Edge functions can't use environment variables, we use JSON config files (config.json) uploaded next to the lambda index.js file to store configuration. The config files can be e.g. generated by Terraform as part of the Lambdas deployment process. Allowed configuration options:

Name Description Type Default Required
environment AWS environment. Feature branch previews are only enabled on staging. We also add X-Robots-Tag which blocks indexing in staging production,staging n/a yes
originBucketName Name of the S3 bucket which stores the cursor files string n/a yes
originBucketRegion AWS region where the above bucket lives (e.g. eu-west-1) at string n/a yes
previewDeploymentPostfix The base part of the app url, e.g. app.staging.pleo.io. Only applicable in staging. string n/a yes
defaultBranchName The name of the default branch of the repo that deploys the app string master no
blockIframes Should the X-Frame-Options custom header be added to block rendering of the app in iframes? bool false no

Details

Lambda@Edge lambdas are used when serving assets from Cloudfront (CDN) distributions. They are triggered in the request/response cycle of a CDN-backed asset at one of the four stages (viewer request, origin request, origin response and origin request). They are invoked with either CloudFrontRequestEvent or CloudFrontResponseEvent event depending on the stage the are associated with.

In this setup the app uses edge lambdas triggered on viewer request and viewer response events. This means that those lambdas will run on every request that is handled by the default distribution behavior which they are associated with.

  • Viewer Request - triggered before the request is sent to the origin (in our case S3 bucket). This lambda can modify which asset is requested from the origin. In our case:

    1. Inspect the Host header of the request and determine which version of the code the user wants to see.

      For example, a request to app.staging.example.com is for the main branch of the app, while a request to my-branch.app.staging.example.com is a request for a feature branch (my-branch) version of the app, and a request to preview-{version}.app.staging.example.com is a request for a specific version.

    2. Based on the above, fetch the cursor file (containing the current active app version) from the S3 origin bucket to figure out which HTML file to request from CDN. The cursor file is updated as part of the CD pipeline.

      For example, for the main branch requested we would fetch deploys/main file from S3, and for a feature branch deploys/my-branch. Then we read its contents, which would be some SHA hash (like e.g. ce4a66492551f1cd2fad5296ee94b8ea2667eac3).

      Note that this is a call to an external data source, and although who chose to store this file in the same bucket as the files served by this CDN distribution, in principle it could be any other data source (API, DynamoDB, etc.). To mitigate the potential performance penalty from making that request (since it's a non-cacheable, blocking request to a resource in a non-edge location) we use a long lived HTTP connection and cache it in global lambda scope which makes it available for consecutive invocations. For the whole discussion of this topic please see AWS's guide to using external data in Edge Lambda.

    3. Once the app version is established, the request object is modified to fetch the right version of the file from the origin bucket.

      For example, we might modify the request to fetch /html/ce4a66492551f1cd2fad5296ee94b8ea2667eac3/index.html following the example above.

      Note that this request is now for an asset with a unique name, that can be fully cached on the CDN level. We upload the file to S3 with max-age=31536000,immutable cache control settings, and in the viewer response lambda we adjust the cache headers when sending the response to the browser to max-age=0,no-cache,no-store,must-revalidate so that the browser doesn't cache the non-unique url locally. This mitigates the latency added by the cursor file request further.

  • Viewer Response - triggered just before the response is returned to the user's browser. Currently we use it set translation-related headers on the response

.well-known files

Well known files under /.well-known URI are exposing some information under a consistent URL across all websites (e.g. .well-known/apple-app-site-association). These are handled in a special way by this module, and should work as expected if placed in the .well-known directory (and not in /static) when uploaded to S3.

Translations

Support for separately served translations can be achieved using the isLocalised config option. When enabled, the viewer request lambda will fetch the latest version of the translations from S3 (from a cursor file assumed present at translation-deploy/latest key in the origin bucket), and pass it to the viewer response lambda which exposes it via a cookie on the response. For more details see the addons/translations.ts file.

Usage

These lambdas as used internally by the Terraform module in this repo. Refer to documentation of the module for more information.