Element 84 Logo

Decision Making @ The Edge

01.30.2019

In July 2017 Amazon Web Services (AWS) made Lambda@Edge available for all customers, giving organizations the ability to run code on the over 100 CloudFront edge locations around the world at the point that is closest to the user. While Lambda@Edge functions are more limited than regular lambda functions, they give developers the ability to do such things such as manipulate user requests before the request makes it to the server, and adding logic to static S3 hosted sites.

Lambda@Edge is feature of CloudFront—AWS’s global Content Delivery Network (CDN). It works by allowing you to add code to handle a request or a response either between the user and the CDN, or between the CDN and the origin. This code can inspect or manipulate requests, modify responses, drop requests, redirect or re-route requests, or just about anything else you can fit into a single function. AWS handles replicating the functions to the edge locations throughout the world and the functions get executed directly on the CloudFront edge locations.

The following diagram shows the four places where you can insert a Lambda@Edge function. Those places are: Viewer Request, Origin Request, Origin Response, and Viewer Response:

Lambda@Edge Function Types

A CloudFront distribution needs to have one or more cache behaviors. These behaviors define how requests to specific URL paths are handled by the CDN. The Lambda@Edge functions are attached to the cache behavior. There is a restriction that you can only have one function of each type per cache behavior. When the function executes, it receives a request or response event in json, and expects a callback with either a request or response object. This allows your code to manipulate these objects, or even change a request to a response and bypass sending the request to the origin. There are some limitations to keep in mind when using Lambda@Edge, but AWS has been relaxing these to make them more like standard Lambda functions.

AWS has plenty of documentation on Lambda@Edge, so let’s jump in and look at some examples of ways that we’ve been able to leverage these functions.

Example 1: Performing a Redirect Without Hitting the Server

In this use case we were running a site that had two different user experiences: one for paid users that included a custom homepage based on their history and preferences, and one for unauthenticated users that featured some free content and gave them a gentle nudge to sign up. The site was a typical Rails server setup. Rails was running on EC2 instances in a single region, behind a load balancer, behind CloudFront. We noticed that we would get large spikes of new users at certain times. These requests would each hit the home page, check their session cookie, and then redirect them to a static homepage. We thought we’d try out Lambda@Edge to keep these spikes from hitting the servers. We hoped it would give the user a better experience by having the redirect issued from the edge location rather than going all the way to the Rails servers.

There are two possible approaches to accomplishing this using Lambda@Edge:

  1. Create a “Viewer Request” function, which executes when a request is received by CloudFront. The function would return an HTTP 302 (temporary redirect) if the user isn’t authenticated.
  2. Use an “Origin Request” function, which executes in between CloudFront and the origin. This could dynamically set the origin to either the authenticated or unauthenticated application in a way that’s seamless to the user.

Example of the “Viewer Request” function with the 302 redirect:


'use strict';

exports.handler = (event, context, callback) => {
    // Get the request and headers from the event
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    // Set the name of the cookie to check, and the URL to redirect to
    const sessionCookie = "SESSION_ID";
    const unauthUrl = "https://unauth.mysite.com";

    console.log("Checking for session cookie");

    // Search the cookies for the session cookie
    if(headers.cookie) {
        for(let cookieIndex = 0; cookieIndex < headers.cookie.length; cookieIndex++) {
            if(headers.cookie[cookieIndex].value.indexOf(sessionCookie) >= 0) {
                console.log("Found session cookie");
                   // Pass the request along unchanged
                   callback(null, request);
                   return;
            }
        }
    }

    // If the cookie is not found, redirect by sending a response object
    console.log("Session cookie not found.  Redirecting.");
    const response = {
            status: '302',
            statusDescription: 'Found',
            headers: {
                location: [{
                    key: 'Location',
                    value: unauthUrl,
                }]
            },
        };
    callback(null, response);
    return;
};

In this example, if the cookie exists then we pass the user’s request along unmodified. If the cookie doesn’t exist, then we craft a response that gives an HTTP 302 along with a Location header to redirect the user’s browser to the unauthenticated page. When the response object is returned from this function, CloudFront won’t continue to call the origin server and will immediately return the request to the user. This results in a quicker response time. The request no longer needs to go to the region where our servers are hosted, and won’t need to be handled by the instances running our application code.

The Lambda@Edge functions are replicated to all of the AWS regions, and executed on the CloudFront edge locations. Like in a typical Lambda function, the console.log() statements are written to CloudWatch logs to be used for debugging, but they are stored in the region where the requests are received. This means that if you have users from different parts of the world accessing your site, your Lambda@Edge logs will be stored in multiple regions.

Example 2: Making an S3 Site Dynamic

For the next use case we have a static site hosted in S3. However, we want to have some logic that controls which pages get displayed based on the user’s location. We use this to display some product information differently for users in different regions.

You could use something like Route 53 Geolocation Routing to achieve this, but we’re looking to just replace one page on a large site. To make this work we’ll be using a Lambda@Edge function on the “Origin Request” to add the appropriate logic. To make the geolocation available to the Lambda function, you must whitelist the CloudFront-Viewer-Country header in the cache behavior.

Example of adding geolocation to an S3 site:


'use strict';

// The list of countries to send to the alternate page
const alternateCountries = ["MX", "FR", "GB", "JP"];

exports.handler = (event, context, callback) => {
    // Get the request and headers from the event
    const request = event.Records[0].cf.request;
    const headers = request.headers;

    // Only check the requests going to this path
    const path = "/page.html";
    const alternatePath = "/page-foreign.html";

    // Grab the geolocation header, and the request’s uri
    const country = headers['cloudfront-viewer-country'];
    const uri = request.uri;

    // If the uri matches the path, and the country is in the list
    // then change the request’s URI
    if (country && uri && uri === path) {
        let currentCountry = country[0].value;
        console.log("Country: " + currentCountry);
        if(alternateCountries.indexOf(currentCountry) >= 0){
            console.log("Using alternate page")
            request.uri = alternatePath;
        }
    } else {
        console.log("Using default page");
    }
    // Pass the request along
    callback(null, request);
};

This example runs before CloudFront makes the request to the origin. At that point, CloudFront will add the geolocation header CloudFront-Viewer-Country which we can then examine to determine the user’s location. Also, rather than redirecting the user to a new page, we can simply modify the existing request by changing the uri parameter, and serve up the correct page transparently to the user. CloudFront can even be configured to cache this response since we’re including the CloudFront-Viewer-Country header as one of the cacheable headers.

Summary

The examples above give us two illustrations of how to use Lambda@Edge for real world applications. We saw functions that execute on “Viewer Request” and “Origin Request” events. However, keep in mind that Lambda@Edge can also execute on responses as well. Some use cases for “Viewer Response” and “Origin Response” would be to modify response headers, or to change an HTTP response codes. Hopefully you will find Lambda@Edge as useful as we have.