Akamai EdgeWorkersでHTTP Response Bodyを変換する

# 1. はじめに
## 1.1. この記事の目的
今日、多様なサービスがインターネットを経由して提供されています。より良いサービスを提供するため、Webサーバーがユーザーに送信するHTTP Response Bodyを動的に変換したい、といったケースは比較的多く存在しているのではないでしょうか。

変換例として、以下のようなものがあげられます。
- （ユーザー体験をモニタリングするための）JavaScriptタグを挿入する
- キャッシュされたResponse Bodyにパーソナライズされたデータを挿入する
- ストリーミング配信におけるメディアマニフェストファイルの中身を変更する

この記事ではAkamai EdgeWorkersを利用してHTTP Response Bodyを変換する方法を紹介します。

なお、この仕組み自体は、Akamai TechDocsに掲載されているEdgeWorkers チュートリアルの1つである"[Response body processing](https://techdocs.akamai.com/edgeworkers/docs/process-response-bodies)"に記載があります。

## 1.2. この記事の対象読者
この記事の対象読者は以下を想定しています。
- HTTP Response Bodyの変換する方法を模索されている方、もしくはそういった方法の提案を行う立場の方
- Akamai EdgeWorkersとAkamai配信設定を実装し検証可能な権限と環境をお持ちの方（必須ではありませんが、本記事を確認しながらご自身の検証環境でハンズオン頂くとより理解が深まる内容になっています）

## 1.3. Akamai EdgeWorkersを利用するメリット
Akamai EdgeWorkersを用いた本処理を行うことで以下のようなメリットを得ることができます。
- 複数のHTMLファイルの修正をすることなく、途中経路上でJavaScriptタグを挿入することで、モニタリング等の機能を簡単に導入することができる
- 今までCDNでキャッシュ配信することが難しかったパーソナライズされたHTMLファイルについて、全ユーザーで共通の部分をキャッシュから配信し、パーソナライズされたデータのみを動的に挿入することで、Webシステムの配信量を削減することができる
- ユーザーごとに最適なコンテンツをあらかじめ複数パターン用意することなく、動的に変換し配信することで、コンテンツ制作負荷の軽減、ならびにWebシステムのコンテンツ格納領域の削減が可能となる

# 2. Webシステム構成概要
## 2.1. Webシステム構成概要と実現可能なこと
まず、Akamai EdgeWorkersを利用する前のWebシステム構成を確認します。
content.htmlというファイルをリクエストすると図のようなテストページが応答されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/9fe884cd-7556-786f-4090-8626f726e30a.png)




次に、Akamai EdgeWorkersを利用した後のWebシステム構成を確認します。
/ew/content.htmlというファイルをリクエストすると先ほどのテストページの「red」が「green」に変換されたページが応答されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/78691305-4afb-5ddf-3f8b-de2ce99bbcc6.png)

:::note info
この記事では、話をシンプルにするために
- 元のコンテンツpath: /content.html
- EdgeWorkersにてBodyが変換されるコンテンツpath: /ew/content.html

のように、pathを別で用意しています。

「元のコンテンツpath」と「EdgeWorkersにてbodyが変更されるコンテンツpath」を、同様のpath（例えば/content.html）とすることも可能です。

その場合、元のコンテンツとEdgeWorkersにより変換されたコンテンツのキャッシュがAkamai CDN上で混ざることが無いような考慮をする必要があります。その方法はこの記事では説明しませんが、こちらの[記事](https://techdocs.akamai.com/edgeworkers/docs/transform-response-content#caching-both-legs-the-correct-way)に考え方の記載があります。
:::

EdgeWorkersはユーザーやAkamai CDNから見た際にあたかもOriginサーバーのように動作します。また、今度は自分自身がユーザーとなりAkamai CDN経由で本当のOriginサーバーに接続します。
Originサーバーよりcontent.htmlを取得し、EdgeWorkersがそれを変換し、/ew/content.htmlのファイルとしてユーザーに応答します。

## 2.2. 変換方式概要
HTTP Response Bodyの変換方式には、大きく2つの方法が存在します。バッファリング方式とストリーミング方式です。それぞれの概要と特徴は以下の通りです。

| 方式 | 概要 | メリット | デメリット |
|:----:|:-----:|:-:|:-:|
| バッファリング方式　　　　　　　　　　　　　| HTTP Response Body全体を１つの変数に格納し、その上で文字列を解析し変換処理を行う  |  操作が簡単 | 操作可能なサイズ上限が低い  |
| ストリーミング方式  | HTTP Response Bodyを小さなチャンクに分割し、1つのチャンクずつ解析し変換処理を行う  | 操作可能なサイズ上限が高い  | 操作が難しい（後述）  |

# 3. EdgeWorkersとAkamai配信設定の実装方法
## 3.1. 前提構成
それでは実際に上記のAkamai EdgeWorkersの仕組みをWebシステム上に構築します。
まずは操作が簡単なバッファリング方式にて実装し、次にストリーミング方式で実装したいと思います。


前述の通りAkamaiのCDNを利用したWebシステムがすでに存在しており、変換対象となるcontent.html（テストページ）も存在していることを前提とします。

この仕組みを実装するにあたりOriginシステム側に変更は必要ありません。AkamaiのCDNで利用している配信設定の変更作業と、EdgeWorkersの新規設定作成により実現可能となります。

## 3.2. EdgeWorkersの実装
### 3.2.1. EdgeWorker IDの作成
まずEdgeWorker IDを作成します。EdgeWorker IDとは、「あるAkamai EdgeWorkersのコードを実行するための実行環境」のようなイメージと捉えてください。実行したいコードごとにEdgeWorker IDを用意し、それぞれのコードをデプロイ/処理させます。

手順は以下のとおりです。

Akamai Control Center（以下ACC）にアクセスします。アクセスしたら左上の☰をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/8bbffb9d-6d78-3602-04bc-d7ec76e17b96.png)
次にCDN > EdgeWorkersリンクをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/19326bd1-d3e3-8b07-37ca-3f487da8b29a.png)

Create EdgeWorker IDをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/c6c54a4e-ea13-8a90-8285-f64929e36bc7.png)
ポップアップしたウィンドウにて必要事項を入力し、Create EdgeWorker IDボタンをクリックします。

（補足）

- Name: 任意の名前を入力します。将来複数のEdgeWorker IDを利用する際の識別性などを考慮します。
- Group: 自身の権限のあるグループの中から選択します。主に本EdgeWorker IDの管理をどのグループに属するユーザーにて実施するかという観点で選択します。
- Contact ID: 基本的に選択肢は1つしかないはずです。複数表示される場合はACC管理者に確認の上選択します。
- Resource tier: Dynamic ComputeとBasic Computeが選択可能です。それぞれリソースの制限が異なります。各リソースの制限は[こちら](https://techdocs.akamai.com/edgeworkers/docs/resource-tier-limitations)より確認可能です。今回はDynamic Computeを選択します。
- Description: 必要に応じ入力します。（空欄でもOK）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/1173343c-df05-7be4-df46-30e5ed4fab59.png)

### 3.2.2. EdgeWorkers コードの作成
次に先ほど作成した、EdgeWorker ID（実行環境）で実際に動作させるコードを作成します。

手順は以下のとおりです。

まず、自身の端末ローカルで以下2つのファイルを作成します。

- マニフェストファイル（bundle.json）
```json
{
    "edgeworker-version": "3.0.0",
    "description" : "Response body processing"
}
```

- ソースファイル（main.js）※ご自身の環境にあわせ「www.example.com」のhost部分は修正してください。
```js
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';

export async function responseProvider(request) {
  var httpResponse = await httpRequest('https://www.example.com/content.html');
  var bufferedBody = await httpResponse.text();

  var modifiedBody = bufferedBody.replace("red", "green");

  return createResponse(200, {}, modifiedBody);
}
```
EdgeWorker IDにこれらのファイルをアップロードするためには、tarで1つのファイルにまとめ、それをgzip圧縮したファイル（拡張子tgz）にする必要があります。これは以下のコマンドにて実行します。（filename.tgzがアップロードするファイルになります）
```
tar -czvf filename.tgz main.js bundle.json
```

次に、ACCに戻り先ほど作成したEdgeWorker IDをクリックします。

Create versionボタンをクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/4bf2a476-99e9-3fa2-b163-7ca740d519df.png)

ポップアップした画面にて、先ほど作成したfilename.tgzをDrag and dropします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/a64b9dc7-d3c0-ade0-bd50-bcbdf38329dc.png)

Drag and dropが完了したら、Create versionボタンをクリックします。（画面では1.0.0となっていますが、上記の手順通りに進めた場合は3.0.0となります。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/912b6dc4-9e9a-a1b3-9930-d979639ec0c8.png)

### 3.2.3. EdgeWorkers コードのStagingデプロイ
コードの作成（EdgeWorker IDにコードを登録する作業）が終わったら、Akamai CDN上で実行する準備をします。

具体的にはAkamai Staging ネットワークへのデプロイを行います。

ここで、Akamai Production / Staging ネットワークについて補足しておきます。Akamai Productionネットワークは本番用プラットフォームです。Akamai CDNを利用した際、エンドユーザーが利用する環境です。Akamai Stagingネットワークは試験用プラットフォームです。エンドユーザーが利用することはありません。特殊な方法でStagingネットワークのEdge ServerのIPを調べ、検証用端末のhostsファイルに手動登録することでアクセスが可能になります。設定方法の詳細はこちらの[記事](https://community.akamai.com/customers/s/article/Stagingrxdxn?language=en_US)をご確認ください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/6fcabd0d-3926-acaa-4b9a-1083dee14910.png)


手順の解説に戻ります。

ACCに戻り、作成中のEdgeWorker IDのコードを作成したVersion（上記手順を実施している場合は3.0.0）の右端Actionsリンクをクリックし、展開されるメニューよりActivate Versionをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/d6450241-4ce7-36f2-af23-fedace87d4db.png)

ポップアップする画面から、Stagingを選択し、Activate versionボタンをクリックします。（ボタンクリック後5分前後でStagingデプロイが完了します）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/bc1924f1-a798-d40f-82f0-70a2b6ad059a.png)

## 3.3. Akamai配信設定の実装
### 3.3.1. Akamai配信設定へのEdgeWorkers Behaviorの追加＆Staging デプロイ
Akamai Stagingネットワークに、コードのデプロイが完了し、EdgeWorkersコードの実行準備が整いました。あとはAkamai CDNの配信設定とこのEdgeWorker IDを紐付けることで利用可能な構成に変更していきます。

手順は以下のとおりです。

EdgeWorkersのコードを実行させたいhost名を持つ配信設定を開き、新規versionを作成します。

その上で以下のようなルールを作成します。作成後、この配信設定もAkamai Stagingネットワークに展開します。

（補足）

- ルール名: 任意の名前をつけます（以下例ではEW_Response body processingという名前にしています）
- Criteria: EdgeWorkersのコードを実行させたい条件を設定します（以下例では、pathが/ew/content.htmlだったら、という条件としています）
- Behaviors: EdgeWorkersビヘイビアを追加/有効化します。また作成したEdgeWorker IDと紐付けます
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/79e1c330-375f-c65a-76da-5ac5b19744a4.png)


## 3.4. 動作検証
EdgeWorkersのコードを実行し動作検証する準備が整いました。早速動作検証をしてみます。

以下のcurlコマンドを利用し、Akamai Stagingネットワークにて動作確認をしてみます。(Akamai Stagingネットワークについては、3.2.3.章参照）
```
curl https://[yourhostname]/ew/content.html --connect-to ::[yourhostname].edgekey-staging.net -H "Pragma: akamai-x-ew-debug" -sD -  -sD -
```
curlコマンドより、以下の結果が出力されます。
```
HTTP/1.1 200 OK
Date: Tue, 01 Oct 2024 02:53:08 GMT
Content-Length: 204
Connection: keep-alive
X-Akamai-EdgeWorker-ResponseProvider-Info: ew=[EdgeWorker ID] v3.0.0:[EdgeWorker ID name]; status=Success
X-Akamai-EdgeWorker-onClientResponse-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-EdgeWorker-onClientRequest-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-Staging: ESSL

<!DOCTYPE html>
<html>
    
    <head>
        <meta charset="utf-8">
        <title>Test page</title>
    </head>
    <body>
        This is test page. The color of the car is green.
    </body>
</html>
```

上記の結果より以下のことがわかります。

- X-Akamai-EdgeWorker-ResponseProvider-Infoヘッダ: 実行されたEdgeWorker ID/version/name、そして実行statusが確認できます
- X-Akamai-Stagingヘッダ: Akamai Stagingネットワークを利用している場合に表示されるヘッダです。Akamai Stagingネットワークに接続できていることが確認可能です
- HTTP Response Body: 「red」が「green」に変換されていることが確認可能です

ちなみに、オリジナルのcontent.htmlは以下の通りです。
```html
<!DOCTYPE html>
<html>
    
    <head>
        <meta charset="utf-8">
        <title>Test page</title>
    </head>
    <body>
        This is test page. The color of the car is red.
    </body>
</html>
```


バッファリング方式におけるHTTP Response Bodyの変換を確認できました。
バッファリング方式はサイズ上限が低い点がデメリットでした。
試しに、content.htmlを100KB以上のファイルに差し替えて検証してみます。

この状態で、前述のcurlコマンドを実行すると、以下の結果が出力されます。
```
HTTP/1.1 500 Internal Server Error
Date: Tue, 01 Oct 2024 03:32:49 GMT
Connection: close
X-Akamai-EdgeWorker-ResponseProvider-Info: ew=[EdgeWorker ID] v3.0.0:[EdgeWorker ID name]; status=Failed
X-Akamai-EdgeWorker-onClientResponse-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-EdgeWorker-onClientRequest-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-Staging: ESSL
```
HTTP Status Codeが500となり、X-Akamai-EdgeWorker-ResponseProvider-Info Headerの値からも、Failed(失敗）であることがわかります。

:::note info
本記事では触れませんが、この[ページ](https://techdocs.akamai.com/edgeworkers/docs/enable-enhanced-debug-headers)を参考にdebugすることでEdgeWorkersの成功/失敗のみならず、失敗の原因が確認可能です。
:::

ちなみに、オリジナルのcontent.htmlは以下の通りです。Test Dataという文字が大量に記述されファイルサイズは100KB以上となっています。
```html
<!DOCTYPE html>
<html>
    
    <head>
        <meta charset="utf-8">
        <title>Test page</title>
    </head>
    <body>
        This is test page. The color of the car is red.
        Test Data
        Test Data
        Test Data
        （省略）
    </body>
</html>
```

:::note info
2024年10月現在、httpRequest()モジュールのtext() メソッドを利用して、オリジンサーバーより得られる応答をバッファリング方式にて処理する際のサイズ上限は128KBです。

また、createResponseモジュールを利用して、ユーザーに応答可能なサイズ上限は100KBです。

オリジンサーバーより得られるcontent.htmlのサイズが128KB以下で、ユーザーに応答する変換後の/ew/content.htmlのサイズが100KB以下であればこの仕組みは問題なく動作します。

最新のリソース制限情報は[こちら](https://techdocs.akamai.com/edgeworkers/docs/limitations)をご確認ください。
:::


## 3.5. ストリーミング方式
それでは、もう一つの方式であるストリーミング方式で試してみたいと思います。

bundle.jsonを編集します。
```json
{
    "edgeworker-version": "3.0.1",
    "description" : "Response body processing"
}
```

main.jsを以下の通り編集します。※ご自身の環境にあわせ「www.example.com」のhost部分は修正してください。
```js
import { ReadableStream, WritableStream } from 'streams';
import { httpRequest } from 'http-request';
import { createResponse } from 'create-response';
import { TextEncoderStream, TextDecoderStream } from 'text-encode-transform';

class FindReplaceStream {
  constructor (find, replace) {
    let readController = null;

    // Create new ReadableStream
    this.readable = new ReadableStream({
      start (controller) {
        // retrive controller for ReadableStream when start() function is called
        readController = controller;
      }
    });

    // Create new WritableStream
    this.writable = new WritableStream({
      // write() function is called on each chunk of text
      // replace content in each chunk and enqueue modified text
      write (text) {
        let modifiedText = text.replace(find, replace);
        readController.enqueue(modifiedText);
      },
      // Close readable stream when writable stream is closed
      close (controller) {
        readController.close();
      }
    });
  }
}

export async function responseProvider (request) {
  // Make httpRequest to retrieve content
  var httpResponse = await httpRequest('https://www.example.com/content.html');

  // Create a response, piping through a chain of transformers
  return createResponse(
    httpResponse.status,
    {},
    httpResponse.body
      .pipeThrough(new TextDecoderStream())
      .pipeThrough(new FindReplaceStream('red', 'green'))
      .pipeThrough(new TextEncoderStream())
  );
}
```

EdgeWorker IDにこれらのファイルをアップロード可能な形式とするため、以下のコマンドを実行します。
```
tar -czvf filename.tgz main.js bundle.json
```

前述の手順により、コードをAkamai Stagingネットワークに展開します。
展開が終わったら前述のcurlコマンドを利用し、動作確認をしてみます。


```
HTTP/1.1 200 OK
Date: Tue, 01 Oct 2024 03:43:07 GMT
Transfer-Encoding:  chunked
Connection: keep-alive
Connection: Transfer-Encoding
X-Akamai-EdgeWorker-ResponseProvider-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-EdgeWorker-onClientResponse-Info: ew=[EdgeWorker ID] v:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-EdgeWorker-onClientRequest-Info: ew=[EdgeWorker ID] v3.0.1:[EdgeWorker ID name]; status=UnimplementedEventHandler
X-Akamai-Staging: ESSL

<!DOCTYPE html>
<html>
    
    <head>
        <meta charset="utf-8">
        <title>Test page</title>
    </head>
    <body>
        This is test page. The color of the car is green.
        TestData
        （省略）
    </body>
</html>
```

100KB以上のファイルですが、問題なく「red」が「green」に変換できていることが確認できました。

:::note info
2024年10月現在、httpRequest()モジュールのbodyオブジェクトを利用して、オリジンサーバーより得られる応答をストリーミング方式として処理する際のサイズ上限は5MBです。

createResponseモジュールにてHTTP Response Bodyをストリーミング方式にてユーザーに応答する場合、サイズに上限はありません。

オリジンサーバーより得られるcontent.htmlのサイズが5MB以下であれば、この仕組みは問題なく動作します。

最新のリソース制限情報は[こちら](https://techdocs.akamai.com/edgeworkers/docs/limitations)をご確認ください。
:::


## 3.6. ストリーミング方式の課題
もう一点、ストリーミング方式には操作が難しいというデメリットがあります。この点について最後に解説したいと思います。
操作が難しい点は2つあります。1つ目がチャンク境界による操作の失敗を回避する必要がある点、2つ目がバックプレッシャーによるメモリ増大を回避する必要がある点です。
### 3.6.1. チャンク境界
ストリーミング方式では、データを細かなチャンクに分割し、チャンク1つずつに対し変換処理を行なっていきます。
以下のようにチャンクが分割された場合、「red」から「green」への変換は問題なく可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/62d6ab6c-a9e1-c5ba-3c6d-64b8598d019a.png)

ただし、以下のようにチャンクが分割された場合、具体的にはredの文字列が2つのチャンクに分断されてしまった場合、「red」から「green」への変換はできません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2728604/d9577147-e4fe-b628-8fac-8b0fbcc7bb2a.png)

この課題を解消する方法例は[こちら](https://github.com/akamai/edgeworkers-examples/tree/master/edgecompute/examples/stream/find-replace-stream)に記載があります。

### 3.6.2. バックプレッシャー
ストリーミング方式では、チャンクごとに変換処理を行います。変換処理が終わったチャンクをユーザーに応答する処理が何らかの理由で遅延した場合、かつサーバーからのチャンク受信は滞りなく進んだ場合、EdgeWorkersのメモリを消費し続ける形でデータが蓄積していきます。
最終的にはEdgeWorkersのメモリ制限に到達し、予期せぬエラーを発生させてしまう可能性があります。

この課題を解消する方法例は[こちら](https://techdocs.akamai.com/edgeworkers/docs/process-response-bodies#backpressure)に記載があります。

## 3.7. 本番リリース
通常の開発作業においては、動作検証（機能テストやリグレッションテスト）が問題なく完了したらAkamai Productionネットワークにデプロイします。
まず、EdgeWorker ID(今回作成したversion）をAkamai Productionネットワークにデプロイします（このタイミングでは本番サービスにアクセス元の国に基づく処理は適用されません）。
次に、Akamai配信設定をAkamai Productionネットワークにデプロイします。このタイミングで本処理が実サービスに適用されます。エンドユーザーも利用可能な構成になります。

# 4. さいごに
このように、Akamai EdgeWorkersを利用することで、HTTP Response Bodyの変換処理を簡単に実装することが可能です。