Lambda@Edge: Why Less is More

Lambda@Edge is a compute service that allows you to write JavaScript code that executes in any of the 150+ AWS edge locations making up the Amazon CloudFront content delivery network (CDN) service.

In this post, I’ll provide some background on CDN technologies. I will also build out an application stack that serves country-specific content depending on where the user request originates from. The stack utilizes a Lambda@Edge function which checks the country code of an HTTP request and modifies the URI to point to a different index.html object within an S3 bucket.

TL;DR: Less time, Fewer resources, Less effort

  • CDNs are ubiquitous. Modern websites and applications make extensive use of CDN technologies to increase speed and reliability.
  • Lambda@Edge has some design limitations: Node.JS only, must be deployed through us-east-1, limitations on memory size differ between event types, etc.

Read on for a working example alongside tips and outside resources to inform you of key design considerations as you evaluate Lambda@Edge.

The best of both worlds: Lambda + CloudFront

  • Fully managed: no servers to manage and you never have to pay for idle
  • Reliable: built-in availability and fault-tolerance
  • Low latency: a global network of 160+ Points of Presence in 65 cities across 29 countries (as of early 2019)

A Use Case

You have a website accessed by users from around the world. For users in the United States, you want CloudFront to serve a website with US market-specific information. The same is true for users in Australia, Brazil, Europe, or Singapore and each of their respective markets. For users in any country besides those mentioned above, you want CloudFront to serve a default website.

Stackery will be used to design, deploy, and operate this stack; but the Infrastructure as Code and Lambda@Edge concepts are valid with or without Stackery.

Check out this link to explore many of the other use cases such as:

  • A/B testing
  • User authentication and authorization
  • User prioritization
  • User tracking and analytics
  • Website security and privacy
  • Dynamic web application at the edge
  • Search engine optimization (SEO)
  • Intelligently route cross origins and data centers
  • Bot mitigation at the edge
  • Improved user experience (via personalized content)
  • Real-time image transformation

Background: Need for Speed

Traffic on the modern Internet has been growing at a breakneck rate over the last two decades. This growth in traffic is being fueled by nearly 4 billion humans with an Internet connection. It’s estimated that more than half of the world’s traffic is now coming from mobile phones and that video streaming accounts for 57.69% of global online data traffic. Netflix alone is responsible for 14.97% of the total downstream volume of traffic across the entire internet! The other half comes from web browsing, gaming, file sharing, connected devices (cars, watches, speakers, TVs) Industrial IoT, and back-end service-to-service communications.

To keep pace with this rate of growth, websites owners and Internet providers have turned to CDN technologies to cache web content on geographically dispersed servers at edge locations around the world. Generally speaking, these CDN’s serve HTTP requests by accepting the connection at an edge location in close proximity to the user (latency-wise), organizing the request into phases, and caching the response content so that the aggregate user experience is fast, secure, and reliable.

When done correctly, the result is a win-win. The end user can expect faster load times, a lighter load on the origin server, and backhaul portion of the major telecommunications networks (ie: the intermediate links between the core network, backbone network, and subnetworks at the edge of the network).

For more background, check out the What is a CDN page from CloudFlare and this Amazon CloudFront Key Features page from AWS.

Lambda@Edge

Lambda@Edge is a relatively new feature (circa 2017) of CloudFront which enables the triggering of Lambda functions by any of the following four CDN events.

Viewer Request

Edge Function is invoked when the CDN receives a request from an end user. This occurs before the CDN checks if the requested data is in its cache.

Origin Request

Edge Function is invoked only when the CDN forwards a request to your origin. If the requested data is in the CDN cache, the Edge Function assigned to this event does not execute.

Origin Response

Edge Function is invoked when the CDN receives a response from your origin. This occurs before the CDN caches the origin’s response data. An Edge Function assigned to this event is triggered even if the origin returns an error.

Viewer Response

Edge Function is invoked when the CDN returns the requested data to the end user. An Edge Function assigned to this event is triggered regardless of whether the data is already present in the CDN’s cache.

When deciding which CDN event should trigger your Edge Function, consider these questions from the AWS Developer Guide, as well as additional clarifying questions from this helpful AWS blog post in the “Choose the Right Trigger” section.

Sample Source

The source code for this project is available from my GitHub.

Template Generation

I used the Stackery editor to lay out the components and generate a template:

The template is available in the Git repo as template.yaml.

This application stack is pretty straightforward: a CDN is configured to serve a default index.html from an S3 bucket and the CDN is also configured to trigger a Lambda@Edge function upon any Origin Request events. Origin Requests are only made when there is a cache miss, but in the context of this application stack, cache misses will be rare. The default TTL for files in CloudFront is 24 hours— depending on your needs, you can reduce the duration to serve dynamic content or increase the duration to get better performance. The latter will also lower the cost because your file is more likely to be served from an edge cache, thus reducing load on your origin.

Pay special attention to lines 11-12 within the infrastructure as code template. These lines configure the CDN to cache based on the CloudFront-Viewer-Country header which is added by CloudFront after the viewer request event.

Also, note that line 23 which specifies the “Price Class 200” for the CDN (which enables content to be delivered from all AWS edge regions except South America). Price Class All is the most expensive which enables content to be delivered from all AWS regions (this is the default if no other price class is specified). Price Class 100 is the cheapest and only delivers content from United States & Canada and Europe. For more information on pricing check out this link.

Lambda@Edge Function

The Lambda@Edge function checks if the country code of the request is AU, BR, EU, SG, or US. If it is, the URI of the HTTP request is modified to point to a specific index.html object (such as au/index.html or us/index.html) within the S3 bucket. The default index.html object is served from S3 bucket if the country code is NOT one of the above five.

Here’s the complete function code: index.js

'use strict'

exports.handler = async (event) => {
    const request = event.Records[0].cf.request
    const headers = request.headers

    console.log(JSON.stringify(request))
    console.log(JSON.stringify(request.uri))

    const auPath = '/au'
    const brPath = '/br'
    const euPath = '/eu'
    const sgPath = '/sg'
    const usPath = '/us'

    if (headers['cloudfront-viewer-country']) {
        const countryCode = headers['cloudfront-viewer-country'][0].value
        if (countryCode === 'AU') {
          request.uri = auPath + request.uri
        } else if(countryCode === 'BR') {
          request.uri = brPath + request.uri
        } else if (countryCode === 'EU') {
          request.uri = euPath + request.uri
        } else if (countryCode === 'SG') {
          request.uri = sgPath + request.uri
        } else if (countryCode === 'US') {
          request.uri = usPath + request.uri
        }
    }
    console.log(`Request uri set to "${request.uri}"`)

    return request
}

Deployment

Of course, Stackery makes it simple to deploy this application into AWS, but it should be pretty easy to give the template directly to CloudFormation. You may want to go through and whack the parameters like ‘StackTagName’ that are added by the Stackery runtime.

Once the deployment is complete, the provisioned CDN distribution will have a DNS address. I deployed this application to several different environments, one of which I have defined as staging. Here’s the DNS address of that distribution: https://d315q2a48nys0i.cloudfront.net/

Lastly, go to the newly created S3 bucket and add this default index.html file to the root of the S3 bucket. Then create the following 5 “folders” in the S3 bucket: au, br, eu, sg, us. I put folders in quotes because it’s not technically a folder, but via the UI, S3 refers to them as folders and allows them to be created as such. Once each folder is created, add the respective index.html that I have saved in this /html directory within this github project (ie: For the au bucket, copy over the index.html that I have saved at /html/au/index.html). The AWS CLI is convenient for this type of copying/syncing, check out this link for tips pertaining to managing S3 buckets and objects from the command line.

If I hit the DNS address of the CDN distribution from Portland Oregon, I see the following:

See How it Appears to the Rest of the World

GeoPeeker is a pretty nifty tool that allows you to see how a site appears to the rest of the world. Just go to this link and geopeeker will show the site I’ve deployed as it appears to users in Singapore, Brazil, Virginia, California, Ireland, and Australia.

Conclusion

I encourage you to explore the shape of the request event object as well as the response event object both of which can be found at this link. At one point prior to finding this page, I was getting my wires crossed in terms of the values available on each object. Once I found it, I was able to instantly get back on track and hone in on the URI value that I wanted to modify.

An alternative implementation to the changing the URI is to change the host. In that scenario, I could have created a separate S3 bucket for the default site and separate S3 buckets for the index.html for each of the 5 countries, then upon each Origin Request, modify the host instead of the URI when I found a match. Perhaps I’ll do that in a follow on post to show the difference in the resulting Infrastructure as Code template and Lambda@Edge function.

The use case I covered is relatively approachable. More advanced use cases, such as securing sites and applications from bots or DDOS attacks, would be really interesting and fun to implement using Lambda@Edge. It would be great to see more blog posts and/or reference implementations based on reproducible Infrastructure as Code samples that show Lambda@Edge based solutions that target A/B testing, analytics, and user authentication and authorization. Let me know on twitter or in the comments which types of use cases your interested in and I’ll work to put them together or coordinate with various serverless experts to bring the solutions to life.

Develop on Serverless With Confidence

Sign up for free and experience cloudside development with Stackery – select and configure services, develop Lambdas locally against live AWS services and manage your serverless apps from pipeline to production.