An Introduction To AWS CloudFront Functions
Tags: aws, serverless, javascript
Published: 2022-03-30 21:00:54

*This article was originally written by [
Ayooluwa Isaiah](https://www.honeybadger.io/blog/authors/ayoisaiah/) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/aws-cloudfront-functions/).*

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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront-pops.png)

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](https://en.wikipedia.org/wiki/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](https://en.wikipedia.org/wiki/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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/functions-event-structure.html) is shown below:

```javascript
{
    "version": "1.0",
    "context": {
        <context object>
    },
    "viewer": {
        <viewer object>
    },
    "request": {
        <request object>
    },
    "response": {
        <response object>
    }
}
```

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.

```javascript
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;
}
```

### 2. Modify an HTTP response

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

```javascript
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;
}
```

### 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.

```javascript
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;
}
```

## Getting started with CloudFront Functions

Let's go ahead and create our first function using the [CloudFront console](https://console.aws.amazon.com/cloudfront/). 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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/GettingStarted.SimpleDistribution.html) to create one.

### Creating the function

### Using the CloudFront console

On the [CloudFront console](https://console.aws.amazon.com/cloudfront/), choose **Functions** on the side navigation, and then click the **Create Functions** button.

![Create CloudFront Functions](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_2.png)

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.

```javascript
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;
}
```

![Save a CloudFront function](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_3.png)

### Using the AWS CLI

Ensure that you have [installed](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) and [configured](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html) 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.

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

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.

```plaintext
{
    "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"
        }
    }
}
```

![AWS CLI being used to create function](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/aws-cli-1.png)

### 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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_4.png)

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:

```json
{
 "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
 }
}
```

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:

```bash
$ aws cloudfront test-function \
      --name security-headers \
      --if-match ETVPDKIKX0DER  \
      --event-object fileb://event-object.json \
      --stage DEVELOPMENT
```

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

```plaintext
{
    "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}}"
    }
}
```

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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/aws-cli-2.png)

### 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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_6.png)

### 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.

```bash
$ aws cloudfront publish-function \
      --name security-headers \
      --if-match ETVPDKIKX0DER
```

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

```plaintext
{
    "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"
        }
    }
}
```

![AWS CLI used to publish functions](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/aws-cli-3.png)

### 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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_7.png)

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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/cloudfront_functions_8.png)

### 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:

```bash
$ aws cloudfront get-distribution-config \
      --id E3GA5OOQ5INAXA \
      --output yaml > dist-config.yaml
```

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:

```yaml
# dist-config.yaml
FunctionAssociations:
  Items:
    - EventType: viewer-response
      FunctionARN: arn:aws:cloudfront::121663830981:function/security-headers
  Quantity: 1
```

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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/aws-cli-4.png)

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:

```bash
$ aws cloudfront update-distribution \
      --id E3GA5OOQ5INAXA \
      --cli-input-yaml file://dist-config.yaml
```

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](https://www.honeybadger.io/images/blog/posts/aws-cloudfront-functions/aws-cli-5.png)

### 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:

```bash
$ 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==
```

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](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions.html) or [FAQs](https://aws.amazon.com/cloudfront/faqs/). A [GitHub repository](https://github.com/aws-samples/amazon-cloudfront-functions) is also available with several examples that you can use as a starting point for building functions.

Thanks for reading, and happy coding!