AkamaiのEdgeWorkers/EdgeKVで動的にキャッシュのオン/オフを切り替えてみる

# はじめに
通常時はCDNのWebキャッシュ設定がno-storeであるコンテンツに対して、必要な時だけ動的にキャッシュ設定をオンにする方法を考えます。
前提として、情報の更新頻度が高いため普段はキャッシュさせたくないが、ユーザーアクセスのピークが発生してオリジンサーバーのリソースが逼迫した時など、一定の条件を満たす場合だけ一時的にキャッシュを有効化し、ピークがおさまればすぐにキャッシュ設定をオフにすることも可能とします。
ユースケースとしてたとえば、スポーツ中継の得点表示やゲームアプリのスコアボード、ニュースサイトを想定してみます。これらのコンテンツはユーザーに対して最新情報を提供するためキャッシュ設定をno-storeとするケースが多いが、イベントやキャンペーン等による大量アクセスの発生により高負荷状態になったオリジンサーバーが応答ができなくなると、そもそもユーザーにコンテンツを提供できなくなる状況に陥ってしまうことが考えられます。対策として、ピークが発生しているコンテンツに対して短いキャッシュTTLで一時的にキャッシュをオンにすることで、オリジンを保護しながら、なるべく新鮮な情報をユーザーに届けることができるようになります。

# EdgeWorkers/EdgeKVとは
Akamaiのエッジサーバー上で独自に開発したJavaScriptのプログラムコードを実行できるサービスがEdgeWorkers、EdgeWorkersと組み合わせて使用可能なKey-Value型のデータベースがEdgeKVです。

参考：[EdgeWorkers:Akamaiのエッジサーバー上でJavascriptで書いたコードが実行可能に](https://www.akamai.com/ja/blog/developers/edgeworker)

# 成果物のイメージ
通常はキャッシュ設定がno-storeとなっている２つのサービスが存在し、それぞれ共通のオリジンサーバーを共有しているものとします。基本的にこれらのサービスは情報の更新頻度が高いためno-storeとしていますが、いずれかのサービス（例：サービス1）においてピークが発生しオリジンへの負荷がかかることで、他サービス(例：サービス2)への影響が発生しうるような環境を想定しています。
- サービス1 - リクエストパスは `/qiita-test/service1/*` とする
- サービス2 - リクエストパスは `/qiita-test/service2/*` とする

今回はゴールとして、サービス1でピークが発生した時を想定し、オリジンサーバーを保護するためにサービス1向けのリクエストだけキャッシュ設定をオン(30秒TTL)にすることを目指します。

# 配信アーキテクチャ
配信のしくみは、以下のとおりです。

① クライアントからエッジに着信したリクエストが、配信設定によりno-storeとして扱われる
② EdgeWorkersのonClientRequestで処理に入る。リクエストパスの2階層目の値(service1 or service2)を抽出し、どのサービスへのリクエストかを判断する
③ 上記②で抽出したリクエスト先のサービスがキャッシュされるべきかどうかEdgeKVの値をチェックする ※
④ EdgeKVが0か1をEdgeWorkersに返す (0はキャッシュなし、1はキャッシュありとする)
⑤ 0であればEdgeWorkersで追加処理はせずに配信設定に戻してリクエストをオリジンへフォワードする。1であればCaching_Flag(Variable)に1を立て、配信設定側でCaching_Flag=1を条件にキャッシュ(30秒TTL)をオンにする

※ 下図の青い矢印のように、オリジンサーバー側で（例えばサーバー負荷に応じて）EdgeKVのValueの値を書き換えることで、サービス単位でキャッシュのオン/オフを行うことを前提とする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321105/eb57d2b9-f44e-4289-795a-3fed41cdf1f1.png)

# 配信設定
親ルールの設定は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321105/007215c3-4837-ca1a-68b1-95144fb61097.png)
子ルールの設定は以下の通りです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/321105/ffff5631-ad47-364f-e161-4ffd37d98fa1.png)

# EdgeKVのデータ
EdgeKVのnamespace名"demo"の中に、グループ名"services"を定義します。"servcies"配下に各サービスをItemとして定義し、それぞれキャッシュをするかしないか判断するためのValueを埋め込んでおきます。
- Value = 0: キャッシュなし(デフォルト)
- Value = 1: キャッシュあり

以下の出力例はEdgeKV向けの[Akamai CLI](https://techdocs.akamai.com/edgekv/docs/akamai-cli)を利用したものです。
```:サービス一覧の表示
% akamai edgekv list items production demo services
----------------------------------------------------------------
--- 2 items from group services were retrieved successfully. ---
----------------------------------------------------------------
service1
service2
```

```:各サービスのValueの表示
% akamai edgekv read item production demo services service1
---------------------------------------------------------------------------------------------------------------
--- Item service1 from group services, namespace demo and environment production retrieved successfully. ---
---------------------------------------------------------------------------------------------------------------
0
% akamai edgekv read item production demo services service2
---------------------------------------------------------------------------------------------------------------
--- Item service2 from group services, namespace demo and environment production retrieved successfully. ---
---------------------------------------------------------------------------------------------------------------
0
```


# EdgeWorkersのコード
リクエストパスの2階層目の値(service1 or service2)から対象サービスを抽出し、そのサービスに対するキャッシュの必要有無をEdgeKVへチェックしにいき、キャッシュが必要であればCaching Flagを1に変更するためのJavaScriptコードを実装しました。([こちら](https://github.com/massuzuk/edgecomputing-demo/tree/main/caching-on-demand)のGithubからサンプルをダウンロード可能)
```
import { EdgeKV } from './edgekv.js';

export async function onClientRequest(request) {
        var default_cachingFlag = 0;
        var cachingFlagFromEdgeKv = 0;
        var servicePath = request.path.split('/')[2];

        const edgeKv = new EdgeKV({namespace: "demo", group: "services"});

        try {
        cachingFlagFromEdgeKv = await edgeKv.getText({ item: servicePath, default_value: default_cachingFlag });
        } catch (error) {
            }

        if (cachingFlagFromEdgeKv == 1){
                request.setVariable('PMUSER_CACHING_FLG',1);
                }else{
                }
}
```


# 動作確認
通常時、サービス1およびサービス2はno-storeでありキャッシュヒットはしません。以下はサービス1にリクエストを送ったときのログです。
```:キャッシュヒットなし
% curl -i -H "Pragma:akamai-x-cache-on"  https://www.example.com/qiita-test/service1/sample.json
HTTP/1.1 200 OK
<SNIP>
X-Cache: TCP_MISS from a23-195-88-204.deploy.akamaitechnologies.com 
<SNIP>
```
サービス1でピークが発生し、オリジンサーバーのリソースが逼迫した場合に、サービス1だけキャッシュをオンになるよう、EdgeKVのサービス1向けのValueを1に変更します。
```:EdgeKVのサービス1向けのValueを1に変更
% akamai edgekv write text production demo services service1 1
-------------------------------------------------------------------------------------------------------------------------
--- Item service1 was successfully created into the environment: production, namespace: msuzuki and groupid: services ---
-------------------------------------------------------------------------------------------------------------------------
```
この状態でサービス1にリクエストが送られるとキャッシュが有効化されています。つまりオリジンサーバーのワークロードを保護できている状態です。
※最初はキャッシュが無いため、2回目以降のリクエストからキャッシュから返される
```:キャッシュヒットあり
% curl -i -H "Pragma:akamai-x-cache-on"  https://www.example.com/qiita-test/service1/sample.json
HTTP/1.1 200 OK
<SNIP>
X-Cache: TCP_MEM_HIT from a23-195-88-204.deploy.akamaitechnologies.com 
<SNIP>
```

# さいごに
今回ご紹介したユースケースは、CDNプラットフォームならではのEdgeComputingの使い方です。簡単なので、ぜひいろいろな応用例を考えてみてください。