DEV Community

Honeybadger Staff for Honeybadger

Posted on • Originally published at honeybadger.io

An Introduction To AWS CloudFront Functions

This article was originally written by
Ayooluwa Isaiah
on the Honeybadger Developer Blog.

Amazon CloudFront is a service that speeds up the distribution and delivery of static and dynamic web content through its global network of machines spread across hundreds of locations, also known as edge locations. CloudFront Functions are an incarnation of FaaS (Function as a Service) and allow you to deploy JavaScript functions to AWS' network of edge locations, to be executed as close as possible to end users.

This new feature allows you to customize or personalize content for your application users closer to where they’re located, thus minimizing network latency. For example, you can transform HTTP headers or API responses to personalize your application for each visitor, implement authentication or encryption logic (such as JWT authentication) to allow or deny requests, or set up URL rewrites and redirects right on the edge.

In this article, we will explore CloudFront Functions in detail, including their purpose, use cases, and how you can get started with writing and deploying your first function.

How CloudFront Functions compare to AWS Lambda@Edge

AWS Lambda@Edge, introduced in July 2017, is an extension of AWS Lambda with capabilities similar to CloudFront Functions, as it allows you to leverage Amazon CloudFront to deliver function results globally. While Lambda@Edge is quite robust, it's not the best choice in many cases, especially those that require a small amount of computation before requests are served up by the CloudFront infrastructure or right before the responses to such requests are dispatched to end users, primarily because Lambda@Edge functions are executed in a regional edge cache (usually in the AWS region closest to the CloudFront edge location reached by the client) instead of the edge location itself.

CloudFront Functions were created to provide a solution better suited for higher volumes and even lower latency since they are executed at the edge location closest to the end user instead of AWS Regions. This makes them ideal for lightweight CloudFront CDN transformations and manipulations that can run on every request to enable latency-sensitive operations at a higher volume. Here's a summary of how CloudFront Functions compare to Lambda@Edge:

  • CloudFront Functions and Lambda@Edge are executed in response to events generated by CloudFront.
  • CloudFront Functions only respond to viewer triggers (when CloudFront receives a request from a viewer or sends a response to a viewer). However, Lambda@Edge can work with both viewer triggers and origin triggers (when CloudFront forwards the request to the origin or receives a response from the origin).
  • Lambda@Edge functions are executed in about 13 regional edge caches (at the time of writing), while CloudFront Functions are executed at 218+ edge locations.
  • CloudFront Functions support JavaScript only, while Lambda@Edge has runtime support for both Node.js and Python.
  • CloudFront Functions can only manipulate HTTP headers. If you need to remove or replace the body of an HTTP request or response, use Lambda@Edge instead.
  • CloudFront Functions do not have access to the network or filesystem, but Lambda@Edge does.
  • CloudFront Functions run for less than one millisecond, while Lambda@Edge can take up to 5 seconds for viewer triggers and 30 seconds for origin triggers.
  • The maximum memory assigned to CloudFront Functions is 2MB, compared to 128MB (viewer triggers) and 10GB (origin triggers) for Lambda@Edge.
  • CloudFront Functions and Lambda@Edge may be used together if you want to manipulate content before and after it’s cached.
  • A free tier of CloudFront Functions is available, unlike Lambda@Edge. The former is also charged per request ($0.1 per million invocations), while the latter is charged per request ($0.6 per million invocations) and function duration ($0.00000625125 for every 128MB-second).

How CloudFront Functions work

CloudFront Functions are natively built into the CloudFront infrastructure with over 218+ points of presence spread across 90 cities and 47 countries. Each of these locations hosts an instance of the Functions runtime, which is an ECMAScript 5.1-compliant JavaScript engine, and each of these runtimes is capable of handling tens of millions of requests per second while delivering sub-millisecond latency.

CloudFront Points of Presence

For security reasons, each function script is well isolated within its own process, with several virtual walls of protection around each process. This model eliminates the cold starts of the virtual machine (VM)-based isolation model used by AWS Lambda and Lambda@Edge, further reducing latency. Individual function scripts are also short-lived, as they run for less than 1ms without any perceptible impact on the performance of the CloudFront CDN.

CloudFront Functions are triggered by events on a specific CloudFront distribution, such as when CloudFront receives a request from a viewer (viewer request) and before CloudFront is about to deliver a response to the viewer (viewer response). You can create new functions from the CloudFront console using the IDE or through the CloudFront CLI. Testing your functions can be done directly against a CloudFront distribution to ensure they will run correctly once deployed.

Use cases of CloudFront Functions

CloudFront Functions are a great way to expand your product's capabilities or completely overhaul the way it performs certain tasks by executing code at the CDN layer instead of on origin servers. By opting to use Functions, you'll be able to build a variety of solutions, such as the following:

  • Serve different content based on the device being used to make the request by rewriting the URL of the request based on the condition you care about. For example, you can send video content at different resolutions to users based on their devices.
  • Implement Geo-targeting to ensure the right content is served depending on the origin country of the end user. For example, you can use this to give Purchasing Power Parity (PPP) Discounts.
  • Inspect or modify any of the request headers before forwarding to the origin or client.
  • Protect the content on your web properties from being hot-linked by other websites.
  • Add security rules and filters to block unwanted visitors and bots.
  • Set up an A/B test by controlling what response is served based on cookies. This helps with testing different versions of a website without changing the URL or redirect.
  • Respond directly (and quickly) from the edge without hitting the origin.
  • Implement access control and authorization for the content delivered through CloudFront and redirect unauthenticated users to login pages.
  • Analyze and track user activity on your website and mobile applications.
  • Add HTTP security headers (such as a Content Security Policy) to all responses without modifying your application code

What CloudFront Functions can do

Each CloudFront function has an entry point called handler. It takes a single argument called event, which is a JSON representation of an HTTP request and response. The basic structure of this event object is shown below:

{
    "version": "1.0",
    "context": {
        <context object>
    },
    "viewer": {
        <viewer object>
    },
    "request": {
        <request object>
    },
    "response": {
        <response object>
    }
}
Enter fullscreen mode Exit fullscreen mode

There are three things a function can do:

1. Modify an HTTP request

You can write a script to modify a client request before being returned to CloudFront for continued processing. For example, you can modify existing request headers or set new ones.

function handler(event) {
    var request = event.request;

    // Modify the request object here.
    request.headers['x-custom-header'] = {value: 'example value'};

    // return modified request to CloudFront for further processing
    return request;
}
Enter fullscreen mode Exit fullscreen mode

2. Modify an HTTP response

With CloudFront Functions, you can modify HTTP response headers before delivery to the client.

function handler(event) {
    var response = event.response;

    // Modify the response object here.
    response.statusDescription = "a description";
    response.headers['x-custom-header'] = {value: 'example value'};

    // return modified response
    return response;
}
Enter fullscreen mode Exit fullscreen mode

3. Create a new HTTP response

You can respond to an HTTP request at the edge without any further processing by CloudFront. Note that you cannot include a response body with CloudFront Functions. If you need to do so, use Lambda@Edge instead.

function handler(event) {
    var request = event.request;

    // Create the response object here
    var response = {
      statusCode: 200,
      "headers": {
          "some-header": {
              "value": "some-value",
          },
      },
    };

    // return response
    return response;
}
Enter fullscreen mode Exit fullscreen mode

Getting started with CloudFront Functions

Let's go ahead and create our first function using the CloudFront console. This function will add a few security headers to each response before it is delivered to the client. Before you proceed, ensure that you have an existing CloudFront distribution or follow the steps in this document to create one.

Creating the function

Using the CloudFront console

On the CloudFront console, choose Functions on the side navigation, and then click the Create Functions button.

Create CloudFront Functions

Enter a function name (such as security-headers), and then click Continue. At this point, you'll be able to write the code for the body of the function. Enter the following into the editor under the Development stage and click the Save button.

function handler(event) {
    var response = event.response;
    response.headers["content-security-policy"] = {
        value: "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;"
    };
    response.headers["x-xss-protection"] = {
        value: "1; mode=block"
    };
    response.headers["feature-policy"] = {
        value: "accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'"
    };
    response.headers["x-frame-options"] = {
        value: "DENY"
    };
    response.headers["referrer-policy"] = {
        value: "strict-origin-when-cross-origin"
    };
    response.headers["x-content-type-options"] = {
        value: "nosniff"
    };

    return response;
}
Enter fullscreen mode Exit fullscreen mode

Save a CloudFront function

Using the AWS CLI

Ensure that you have installed and configured the AWS CLI with the right credentials for your account before proceeding.

Write the function code listed above in a function.js file created somewhere in your filesystem, and then use the command below to create the function on CloudFront.

$ aws cloudfront create-function \
      --name security-headers \
      --function-config Comment="Security headers function",Runtime="cloudfront-js-1.0" \
      --function-code fileb://function.js
Enter fullscreen mode Exit fullscreen mode

If everything goes well, you'll get the following output describing the function that you just created. Note and copy the ETag value, as it will be used to identify this function in subsequent sections.

{
    "Location": "https://cloudfront.amazonaws.com/2020-05-31/function/arn:aws:cloudfront::121663830981:function/security-headers",
    "ETag": "ETVPDKIKX0DER",
    "FunctionSummary": {
        "Name": "security-headers",
        "Status": "UNPUBLISHED",
        "FunctionConfig": {
            "Comment": "Security headers function",
            "Runtime": "cloudfront-js-1.0"
        },
        "FunctionMetadata": {
            "FunctionARN": "arn:aws:cloudfront::121663830981:function/security-headers",
            "Stage": "DEVELOPMENT",
            "CreatedTime": "2021-06-06T14:40:49.261000+00:00",
            "LastModifiedTime": "2021-06-06T14:40:49.261000+00:00"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

AWS CLI being used to create function

Testing the function

Using the CloudFront console

Once you've created the function, you can test it by providing an event object that is representative of the type of HTTP requests or responses that your CloudFront distribution would receive in production.

On the function page, click the Test tab. Select the event type (Viewer Response), the Stage (Development) and a sample event (Viewer response with headers). You can modify this sample event (or create one from scratch) through the options in the Input section. The JSON tab is particularly handy for copying the JSON representation of the event object for testing through the AWS CLI or CloudFront API. You don't need to change anything here, so go ahead and click the Test button to run the test.

Test a CloudFront function

Once the test runs, you will see a success message at the top of the screen (or a failure message if the test fails). Under the Output section, under Compute utilization, you'll see a number that indicates the length of time that the function took to run as a percentage of the maximum allowed time. For example, a compute utilization of 34 means that the function completed in 34% of the maximum allowed time.

Using the AWS CLI

To test functions using the AWS CLI, you need to create a JSON file (such as event-object.json) and pass it to the test-function subcommand. Here's the JSON object that mirrors the Viewer response with headers sample event on the CloudFront console:

{
 "version": "1.0",
 "context": {
  "eventType": "viewer-response"
 },
 "viewer": {
  "ip": "1.2.3.4"
 },
 "request": {
  "method": "GET",
  "uri": "/index.html",
  "querystring": {
   "test": {
    "value": "true"
   },
   "fruit": {
    "value": "apple",
    "multiValue": [
     {
      "value": "apple"
     },
     {
      "value": "banana"
     }
    ]
   }
  },
  "headers": {
   "host": {
    "value": "www.example.com"
   },
   "accept": {
    "value": "text/html",
    "multiValue": [
     {
      "value": "text/html"
     },
     {
      "value": "application/xhtml+xml"
     }
    ]
   }
  },
  "cookies": {
   "id": {
    "value": "CookieIdValue"
   },
   "loggedIn": {
    "value": "false"
   }
  }
 },
 "response": {
  "statusDescription": "OK",
  "headers": {
   "server": {
    "value": "CustomOriginServer"
   },
   "content-type": {
    "value": "text/html; charset=UTF-8"
   },
   "content-length": {
    "value": "9593"
   }
  },
  "cookies": {},
  "statusCode": 200
 }
}
Enter fullscreen mode Exit fullscreen mode

Once saved, pass the JSON file to the test-function subcommand, as shown below. Ensure that you replace the value of the --if-match flag with the ETag value that you copied in the previous section:

$ aws cloudfront test-function \
      --name security-headers \
      --if-match ETVPDKIKX0DER  \
      --event-object fileb://event-object.json \
      --stage DEVELOPMENT
Enter fullscreen mode Exit fullscreen mode

If the command is successful, you will see output similar to the one shown below, which shows the results of testing the function.

{
    "TestResult": {
        "FunctionSummary": {
            "Name": "security-headers",
            "Status": "UNPUBLISHED",
            "FunctionConfig": {
                "Comment": "Security headers function",
                "Runtime": "cloudfront-js-1.0"
            },
            "FunctionMetadata": {
                "FunctionARN": "arn:aws:cloudfront::121663830981:function/security-headers",
                "Stage": "DEVELOPMENT",
                "CreatedTime": "2021-06-06T14:40:49.261000+00:00",
                "LastModifiedTime": "2021-06-06T14:40:49.333000+00:00"
            }
        },
        "ComputeUtilization": "27",
        "FunctionExecutionLogs": [],
        "FunctionErrorMessage": "",
        "FunctionOutput": "{\"response\":{\"headers\":{\"server\":{\"value\":\"CustomOriginServer\"},\"content-length\":{\"value\":\"9593\"},\"content-security-policy\":{\"value\":\"default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;\"},\"x-content-type-options\":{\"value\":\"nosniff\"},\"x-xss-protection\":{\"value\":\"1; mode=block\"},\"x-frame-options\":{\"value\":\"DENY\"},\"referrer-policy\":{\"value\":\"strict-origin-when-cross-origin\"},\"content-type\":{\"value\":\"text/html; charset=UTF-8\"},\"feature-policy\":{\"value\":\"accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'\"}},\"statusDescription\":\"OK\",\"cookies\":{},\"statusCode\":200}}"
    }
}
Enter fullscreen mode Exit fullscreen mode

Note the following about the output:

  • FunctionSummary describes the tested function.
  • ComputeUtilization indicates the length of time that the function took to run as a percentage of the maximum allowed time.
  • FunctionOutput is the object that the function returned. As you can see, the output object includes the security headers that were set in the function code, which proves that the function is working as intended.
  • FunctionErrorMessage will contain an error message if the test was unsuccessful.

AWS CLI used to test a function

Publishing the function

Using the CloudFront console

After testing your function thoroughly, you can move to the Publish tab to copy the function from the development stage to the live stage. All you need to do is click the Publish button (or Publish and update if updating a function).

Publishing a CloudFront function

Using the AWS CLI

The aws cloudfront publish-function command will publish the function that matches the name and ETag value that was passed to the --name and --if-match options, respectively.

$ aws cloudfront publish-function \
      --name security-headers \
      --if-match ETVPDKIKX0DER
Enter fullscreen mode Exit fullscreen mode

Here's the output you can expect to get if the publishing succeeds:

{
    "FunctionSummary": {
        "Name": "security-headers",
        "Status": "UNASSOCIATED",
        "FunctionConfig": {
            "Comment": "Security headers function",
            "Runtime": "cloudfront-js-1.0"
        },
        "FunctionMetadata": {
            "FunctionARN": "arn:aws:cloudfront::121663830981:function/security-headers",
            "Stage": "LIVE",
            "CreatedTime": "2021-06-06T15:15:00.413000+00:00",
            "LastModifiedTime": "2021-06-06T15:15:00.413000+00:00"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

AWS CLI used to publish functions

Associating the function with a CloudFront distribution

Using the CloudFront console

Click the Associate tab and select the Distribution, Event type (Viewer Response in this case), and Cache behavior. Afterwards, click Add association and confirm in the dialog.

Associating a CloudFront function

A banner should appear at the top of the page confirming the successful association with the distribution. You can also see the function association under Associated CloudFront distributions.

Associated CloudFront distributions

Using the AWS CLI

To associate a CloudFront Function with an existing distribution using the AWS CLI, get the distribution ID from the console and pass it to the --id flag of the aws cloudfront get-distribution-config command, as shown below:

$ aws cloudfront get-distribution-config \
      --id E3GA5OOQ5INAXA \
      --output yaml > dist-config.yaml
Enter fullscreen mode Exit fullscreen mode

If successful, the above command will not display any output. However, you should see a newly created dist-config.yaml file in the current directory, which should be opened in your favorite text editor. Edit the file as described below:

  • Change the Etag field to IfMatch but leave the value unchanged.
  • Find the FunctionAssociations field and update it, as shown below:
# dist-config.yaml
FunctionAssociations:
  Items:
    - EventType: viewer-response
      FunctionARN: arn:aws:cloudfront::121663830981:function/security-headers
  Quantity: 1
Enter fullscreen mode Exit fullscreen mode

Replace the value of FunctionARN above with the FunctionARN field of the appropriate function retrieved by running aws cloudfront list-functions in the terminal. You can also change viewer-response to viewer-request if this is what your function needs to be triggered on. For the security-headers function, viewer-response is appropriate. Once you're through with the edits, save the file.

FunctionARN in AWS CLI output

Finally, use the aws cloudfront update-distribution command to update the specified distribution with the contents of the dist-config.yaml file as shown below:

$ aws cloudfront update-distribution \
      --id E3GA5OOQ5INAXA \
      --cli-input-yaml file://dist-config.yaml
Enter fullscreen mode Exit fullscreen mode

After running the command, some output that describes the distribution that was just updated will be printed to the console. The Status of the distribution will change to InProgress while the distribution is redeployed, which typically takes a few minutes.

AWS CLI used to associate function with distribution

Verifying the function

Now that the function has been published and associated with a CloudFront distribution, it's time to confirm that it works correctly. You may use curl or the browser to make a request to a resource that exists on your CloudFront distribution, as shown below:

$ curl --head https://d2sbyrn254rio7.cloudfront.net/doc.html
HTTP/2 200
content-type: text/html
content-length: 0
date: Tue, 01 Jun 2021 13:43:26 GMT
last-modified: Tue, 01 Jun 2021 13:42:40 GMT
etag: "d41d8cd98f00b204e9800998ecf8427e"
accept-ranges: bytes
server: AmazonS3
via: 1.1 e792582e94d051796ee83e4a94038f8e.cloudfront.net (CloudFront)
content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net;
x-xss-protection: 1; mode=block
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
feature-policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none'
x-content-type-options: nosniff
x-cache: Hit from cloudfront
x-amz-cf-pop: JFK51-C1
x-amz-cf-id: 84P8wPkvE7TjGl_ssjryL-6vmkW1dhaeH4gaoHZv7A6BPzk4lbVlWg==
Enter fullscreen mode Exit fullscreen mode

Notice that all the response headers added by the function code are included in the response. This proves that the function is working correctly.

Conclusion

CloudFront Functions are a great way to implement high-volume CDN customizations that can run on every request, enabling you to deliver richer and more personalized content to your end users at low latency. I hope this introduction has helped you figure out how you can leverage them in your applications.

To learn more about CloudFront Functions, refer to the CloudFront Developer Guide or FAQs. A GitHub repository is also available with several examples that you can use as a starting point for building functions.

Thanks for reading, and happy coding!

Top comments (0)