Augment Image and Video Manager with edge compute

This use case describes how you can use EdgeWorkers and EdgeKV to augment the existing functionality found in our Image and Video Manager product. Using only 20 lines of JavaScript code we'll deliver higher-quality images, decrease image byte size, and improve performance metrics.

In this example we make the assumption that we have already lowered the compression level of all the smaller images on our site to retain as much visual quality as possible. Now we want to increase the compression rate for larger images to save even more bytes without degrading the visual quality. You can find more information on what we are trying to do in the perceptual compression previews documentation in the Image and Video Manager documentation.

👍

You can find the complete code samples for this use case in the EdgeKV GitHub repo.

Implementation

To get the size of the original image, we store the Content-Length header value that we receive when fetching the original image for optimization from the origin server.

We then use an EdgeWorkers function to read this stored data from images created for delivery to the browser. With Akamai acting as a reverse proxy, different traffic flows require execution of different rules depending on whether a request is from a client, is a derivative request, or is a pristine request from Akamai to the origin server.

On a high level, the flow of our code using EdgeWorkers and EdgeKV looks like this:

Image and Video Manager workflow

  1. A user requests an image object for the first time from an edge server.

  2. The edge server relays the image request to the image server. Before that, the derivative EdgeWorkers function executes, but renders no value from EdgeKV.

  3. The image server requests the pristine image from the origin server. This request is made through another edge server so we are able to execute operations on the returned image.

  4. The edge server receives the pristine image object from the origin server.

  5. The edge server triggers the pristine image EdgeWorkers function. It stores the Content-Length header value in EdgeKV for the URL. The image server also stores the image object for processing and optimization.

    The user triggering the initial image request is served a derivative image at this stage. It includes transformation and default optimization, but does not include changes based on the pristine Content-Length header as that value is needed as input before the pristine request.

  6. Another user requests the same image object from the edge server.

  7. The derivative EdgeWorker triggers again, and now returns a value from EdgeKV.

  8. Based on the value returned, the edge server requests the optimal image transformations for the specific image from the image server. The optimized image is then delivered to the client.

The first step to build this uses Akamai’s primary configuration tool, Property Manager. We setup Property Manager in our delivery configuration to inform it which requests the EdgeWorkers functions will respond to.

📘

You cannot use URL’s as keys, as EdgeKV supports a limited character set for naming a key. To overcome this limitation, we used a hash operation on the URL using the SHA-1 hashing function. You can exclude the protocol from the hash value as the image size will not differ. Hashing operations can be accomplished using Property Manager, or by importing a crypto library within the EdgeWorkers function. For simplicity and to minimize the EdgeWorkers code bundle, we used Property Manager to do the hashing in this example.

Property Manager Configuration

Update the delivery configuration in Property Manager as follows:

Add the property variables

  1. Create two user-defined variables in the “Property Variables” panel:
    1. One variable will store the Content-Length header from the origin server response. Image and Video Manager performs operations with input from presets or URL-provided instructions.
    2. A second variable to store the URL hash. In this case, we'll also configure Property Manager to calculate the SHA1 hash.

Image and Video Manager user variables

  1. The Image and Video Manager behavior is usually found in a rule at the bottom of the Property Manager configuration within an image file extension match (see screenshot below).

Image and Video Manager behavior

This rule invokes Image and Video Manager transformations and delivery operations for the specified file extensions.

👍

You need to define any instructions that Image and Video Manager should use as input before this rule. Any rules we now will add need to be in the correct order. This is a simplified example rule list, a configuration usually contains many more rules.

Invoke Image and Video Manager

Add rules

  1. Click Add Rule to create an empty parent rule container and name it: “Set quality based on Content-Length”.

    This name, in combination with the optional comment field, lets you provide an overview of what the rules do with an incoming request.

  2. Select the empty container rule and click the drop down arrow next to Add Rule. Select Child rule and use the preselected Blank Rule Template, naming it as shown above.

  3. Repeat this for the containers that we will configure with a match condition, and the behaviors that the server should execute if the condition is true. Do this by clicking Add Match and Add behavior respectively.

  4. Next, populate the first two rules with one EdgeWorkers behavior each.

    Remember to prepend these behaviors with the URL hash operation so it's available for use in the EdgeWorker code. In our example (displayed below) with the Set Variable behavior, the hash will include the hostname, path and query parameter.

  5. Only invoke one behavior on requests to the backend. To do this, select the Image and Video Manager match combined with the value Pristine request only. A pristine image is the untouched original from the backend storage

Add behaviors to your rules

To add a behavior to a rule you need to first create an EdgeWorker ID. The EdgeWorker ID is a unique identifier for a given EdgeWorker. The EdgeWorkers behavior should list the EdgeWorker IDs that you've already created.

  1. For this rule select the EdgeWorker ID that executes on origin requests and stores the Content-Length header as EdgeKV data (see image below).

Invoke pristine EdgeWorker function

  1. Invoke the second rule and behavior on client requests only by using an "is not" match rule.

    A negative match in this case is simply the opposite of the rule above. This means that any request that does not target the backend image should invoke the EdgeWorker code bundle that in turn reads EdgeKV data.

Invoke derivative EdgeWorker

  1. When the EdgeWorkers function executes on the client request, it populates the EdgeKV database. This sets the value of the user variable PMUSER_PRISTINE_SIZE accordingly.

  2. We can now use that value as an evaluation criteria to set any desired Image and Video Manager parameter as input for the upcoming transformation in the Image Manager behavior. In this example, we set a policy for Image Manager that uses the lowest compression available for any images that are smaller than 20Kb. The image data itself comes from EdgeKV.

📘

The range includes 0. This is because in the EdgeWorkers JavaScript code (described further down) we set a default value to be able to override the policy settings of Image and Video Manager even if EdgeKV is not yet populated for the URL hash. Use caution when implementing this optional step, as you might inadvertently deliver unnecessarily large images to the end user.

Sett Image and Video Manager policy parameter

EdgeKV Setup

To initialize and setup EdgeKV, you can use the EdgeKV CLI or the EdgeKV Management API.

  1. We'll use the CLI for this example, starting with the command to initialize a new key-value store database:

    akamai edgekv initialize

  2. These commands create a default namespace. In this case, we want to use a specific namespace, so we create another namespace called “im” (short for Image Manager). We will do this with the CLI for the staging network:

    akamai edgekv create ns staging im --retention 90 --groupId 0

    And the production network:

    akamai edgekv create ns production im --retention 90 --groupId 0

We don't need to create a group to store the items in. This will happen automatically if it does not yet exist during the first write operation by the EdgeWorkers code.

EdgeWorkers code

If you are new to EdgeWorkers and EdgeKV, it may be helpful to review the EdgeWorkers documentation and this EdgeKV documentation.

Before you start, review the Getting Started section of the EdgeWorkers documentation. You can follow the steps in the Hello World tutorials to learn how to:

  • Create an EdgeWorkers code bundle.
  • Create an EdgeWorker ID.

👍

Make sure you name your EdgeWorker ID appropriately and choose the group where your Property Manager configuration resides. You also need to select Dynamic Compute as the resource tier to use EdgeWorkers with EdgeKV.

  • Create a version and upload your code bundle.
  • Activate the version on staging or production.

As we mentioned earlier, one EdgeWorkers function will trigger on backend requests only when fetching the pristine image for the first time. Let's walk through the backend-focused code to understand what the EdgeWorkers function is doing. Note that you can find the complete code in the EdgeKV GitHub repo.

import { EdgeKV } from "./edgekv.js"; //include this file from the parent repository. https://github.com/akamai/edgeworkers-examples/blob/master/edgekv/lib/edgekv.js
import { logger } from "log";
 
const edgeKv = new EdgeKV({ namespace: "im", group: "pristine" });
 
export function onClientResponse(request, response) {
 if (response.status === 200) {
   let key = request.getVariable("PMUSER_PATH_SHA1");
   let size = 0;
   let header = response.getHeader("Content-Length");
 
   if (header) {
     size = header[0];
 
     try {
       edgeKv.putText({ item: key, value: size });
     } catch (error) {
       logger.log(`EKV: ${error.toString()} - Key is: ${key}`);
     }
   }
 }
}
  1. Import the helper library that enables communication with the EdgeKV key-value store. This lets us perform CRUD operations with just a few lines of code. We'll also import the logger library for troubleshooting purposes.

    import { EdgeKV } from "./edgekv.js";
    import { logger } from "log";

  2. Next, define an EdgeKV object targeting the previously-created namespace.
    Here we instantiate a new EdgeKV object and pass it the namespace of the EdgeKV store (“im”) and the group that will contain the pristine image data (“pristine”). Note that these names are arbitrary, and are used as an example.

    const edgeKv = new EdgeKV({ namespace: "im", group: "pristine" });

  3. The first function will handle the response from the backend for the pristine request. For this function we will use the onClientResponse EdgeWorkers event handler. This function includes both the request and response as attributes, which we use later in the code.

    onClientResponse works well if we have a high offload (cache hit ratio) on the pristine object, and want to fill the EdgeKV database with values as quickly as possible. If we prefer a low amount of EdgeWorkers triggers, we can change the function to onOriginResponse instead. If the offload of the pristine objects is lower, either case will typically trigger a similar amount of EdgeWorkers events.

    export function onClientResponse(request, response) {

  4. We only want to execute the code on successful pristine responses, avoiding storing faulty data in EdgeKV. To accomplish this, we verify that the response code from the backend is 200, which translates to a successful request and response.

    if (response.status === 200) {

  5. Next, we define a variable called “key” which will be populated with the hashed URL from the incoming request object:

    let key = request.getVariable("PMUSER_PATH_SHA1");

  6. The next variable is “size” and will be populated with the Content-Length header from the incoming response object. Before populating the variable, we validate that the header was returned.

let size = 0;
let header = response.getHeader("Content-Length");
if (header) {
     size = header[0];
  1. Store the size value as the hash key in the pristine group. To optimize performance use a method to continue code execution immediately after sending the data to EdgeKV, instead of waiting for a response as this is not needed in the pristine request step. For more information on how this works in detail you may refer to the Promise.catch() documentation. Any errors encountered are logged for troubleshooting purposes.
try {
       edgeKv.putText({ item: key, value: size });
     } catch (error) {
       logger.log(`EKV: ${error.toString()} - Key is: ${key}`);
     }

Client-edge EdgeWorkers code

The second EdgeWorkers function is similar, but includes a few important differences as it needs to execute on the derivative image.

  1. Use this code during the onClientRequest event handler to apply the transformations on incoming client requests to the edge server. Unlike the pristine EdgeWorkers code, this needs to run as an asynchronous function because we have to await the response of the EdgeKV value before continuing:

    export async function onClientRequest(request) {

  2. Declare the previously discussed default size value to use if the EdgeKV database isn’t yet populated, or returns an error:

    let size = 0;

  3. Create a try/catch block where the key variable is defined as the SHA1 hash value we stored in Property Manager.

  4. Retrieve the size value from EdgeKV as an individual item (key-value pair).

    To ensure we do not overwrite the default size value with null if returned by EdgeKV, we validate it with an if statement. In the event of an error, we log an error message with the key itself concatenated with the related error information. This will help for troubleshooting activities.

try {
   let key = request.getVariable("PMUSER_PATH_SHA1"); //this variable is set in Property Manager for the delivery configuration and contains the hashed URL, protocol+domain typically excluded
   let value = await edgeKv.getText({ item: key });
   if (value !== null) {
     size = parseInt(value) || 0;
   }
 } catch (error) {
   logger.log(`EKV: ${error.toString()} - Key is: ${key}`);
 }
  1. Set the value in the previously-defined pristine variable. This determines whether to invoke the specific Image and Video Manager parameter. The value is then picked up by our third Property Manager rule, “Set IM Policy Parameter”, which we added in the final step of the Property Manager configuration.
request.setVariable('PMUSER_PRISTINE_SIZE', size);
  1. Here's the complete EdgeWorkers code for the client request:
import { EdgeKV } from "./edgekv.js"; //include this file from the parent repository. https://github.com/akamai/edgeworkers-examples/blob/master/edgekv/lib/edgekv.js
import { logger } from "log";
 
const edgeKv = new EdgeKV({ namespace: "im", group: "pristine" });
 
export async function onClientRequest(request) {
 let size = 0;
 try {
   let key = request.getVariable("PMUSER_PATH_SHA1"); //this variable is set in Property Manager for the delivery configuration and contains the hashed URL, protocol+domain typically excluded
   let value = await edgeKv.getText({ item: key });
   if (value !== null) {
     size = parseInt(value) || 0;
   }
 } catch (error) {
   logger.log(`EKV: ${error.toString()} - Key is: ${key}`);
 }
 request.setVariable("PMUSER_PRISTINE_SIZE", size); //this is the variable that will be used by Image Manager, also defined in Property Manager
}

Design considerations

Looking back, we had a few things to consider as we began this project:

  • What to hash: To avoid storing unnecessary hashes and to stay within the EdgeKV data limits, we used care when storing dynamic URLs. An example of a dynamic URL would be user- specific query parameters added to the end of a URL. Similar to the protocol in a URL, apart from transformation instructions, query parameters typically have no effect on the image delivered, hence no effect on the actual value we want to store. A best practice when deciding what to hash is to use the minimum common denominator value for the hash, and exclude all other parts of the URL.

  • Updates: The hash value should be automatically updated if the image has changed. An image can change when a refresh occurs after an image asset TTL expires or when a purge operation executes on the Akamai cache.

  • Cold start: For a short period of time, requests for images are served without input from EdgeKV as the database writes data throughout the network. Whichever Image Manager parameter that is going to be set will not follow with these first requests. This is why it's important to take care around which parameters to utilize.

  • Offload ratio: This solution works best for content with a fairly high offload ratio. Added latency can occur if there is long-tail content that is not often available in cache or if the application produces a lot of user-generated images. It's also possible that EdgeKV will not yet have the stored value, as mentioned above. This can lead to sub-optimal images being delivered to the user.

📘

There can be some additional latency on the first request for a new image that has not yet been seen by our EdgeWorker code before.

This impact would be even lower or negated completely if we increase the compression ratio of the images as described earlier.