Cloudflare Workers からMomento Cacheを呼び出す
先日Momento社からこんな発表がありました。
早速Twitterではこんな反応があり、呼ばれました。
https://twitter.com/yoshii0110/status/1682210506348834816?s=20
会場とりました♡、募集ページ作ります♡、とあれよあれよという間にイベントを9月28日にやる予定に。
もしかして、これ、、、誰もハンズオンシナリオ作るつもりないのでは・・・と思っていたら、案の定私がやることに(笑)
CloudflareUGの方からもやりたいとお願いされたので、じゃあということで簡単に検証してみました。
前からMomentoも少し触ってみたかったので良しとしましょう。
Momentoとは
使いやすいサーバレスのキャッシュとして最近よく名前を聞きます。真のマネージドサーバレスサービス真としてElastiCacheなどより簡単にキャッシュ環境を作れるようです。そのキャッシュに対してデータ書き込み、読み込み、削除オペレーションがCloudflare Workersから出来るようになったというのが今回の発表のようです。
さっそくやってみる
まずはWorkersの開発環境として、https://zenn.dev/kameoncloud/articles/1fac9762aab4ec
をやってHello Worldまでやっておきます。
その後Momentoの環境をセットアップします。手順は自社サービスではないので割愛しますがここにあります。
なんと日本語手順がありました。初めて見たときの感想は(ふふん、やるじゃないの)とった感じです。Cacheの名前はdemo-cache
にしておいて下さい。
Workers 側の作業
ではここからWorkers側の作業です。手順は以下のMomentoが管理しているGitHubにあります。
https://github.com/momentohq/client-sdk-javascript/tree/main/examples/cloudflare-workers
HTTP-API
とWEB-SDK
と両方あるようですが、今回はHTTP-API
を試します。
wranglerが使える環境でまずは以下を実行します。
git clone https://github.com/momentohq/client-sdk-javascript.git
cd client-sdk-javascript/examples/cloudflare-workers/http-api
npm install
次にwrangler.toml
を以下のように編集します。
name = "momento-cloudflare-worker-http"
main = "src/worker.ts"
compatibility_date = "2023-07-10"
[vars]
MOMENTO_REST_ENDPOINT = "https://api.cache.cell-ap-northeast-1-1.prod.a.momentohq.com"
MOMENTO_CACHE_NAME = "demo-cache"
Momento のGitHubだと少しだけ説明不足で環境変数のセット関連で少しはまってしまいましたが、#
を削除します
次に.dev.vars
にJSONに入っているトークンを入力します。
MOMENTO_AUTH_TOKEN="<token>"
"
は必要なので残しておきます。
公式の手順だと次にnpm run start
となっていますが飛ばします!(やっても、勿論問題ないです)
npx wrangler secret put MOMENTO_AUTH_TOKEN
を実行し、JSONに含まれているトークンをcopyしてCloudflare の Secret Storeにトークンを格納しておきます。これによりWorkresスプリプト内でMOMENTO_AUTH_TOKENでトークンを呼び出すことができます。
次にworkers.tsを以下で置換します。
/**
* Welcome to Cloudflare Workers! This is your first worker.
*
* - Run `npm run start` in your terminal to start a development server
* - Open a browser tab at http://localhost:8787/ to see your worker in action
* - Run `npm run deploy` to publish your worker
*
* Learn more at https://developers.cloudflare.com/workers/
*/
class MomentoFetcher {
private readonly apiToken: string;
private readonly baseurl: string;
constructor(token: string, endpoint: string) {
this.apiToken = token;
this.baseurl = `${endpoint}/cache`;
}
async get(cacheName: string, key: string) {
const resp = await fetch(`${this.baseurl}/${cacheName}?key=${key}&token=${this.apiToken}`);
if (resp.status < 300) {
console.log(`successfully retrieved ${key} from cache`)
} else {
throw new Error(`failed to retrieve item from cache: ${cacheName}`)
}
return await resp.text();
}
async set(cacheName: string, key: string, value: string, ttl_seconds: number = 30) {
const resp = await fetch(`${this.baseurl}/${cacheName}?key=${key}&token=${this.apiToken}&&ttl_seconds=${ttl_seconds}`, {
method: 'PUT',
body: value
});
if (resp.status < 300) {
console.log(`successfully set ${key} into cache`);
} else {
throw new Error(`failed to set item into cache message: ${resp.statusText} status: ${resp.status} cache: ${cacheName}`);
}
return;
}
async delete(cacheName: string, key: string) {
const resp = await fetch(`${this.baseurl}/${cacheName}?key=${key}&token=${this.apiToken}`, {
method: 'DELETE',
});
if (resp.status < 300) {
console.log(`successfully deleted ${key} from cache`);
} else {
throw new Error(`failed to delete ${key} from cache. Message: ${resp.statusText}; Status: ${resp.status} cache: ${cacheName}`);
}
return resp;
}
}
export interface Env {
MOMENTO_AUTH_TOKEN: string;
MOMENTO_REST_ENDPOINT: string;
MOMENTO_CACHE_NAME: string;
}
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const client = new MomentoFetcher(env.MOMENTO_AUTH_TOKEN, env.MOMENTO_REST_ENDPOINT);
const cache = env.MOMENTO_CACHE_NAME;
const key = "key";
const value = "value";
// setting a value into cache
const setResp = await client.set(cache, key, value);
console.log("setResp", setResp);
// getting a value from cache
const getResp = await client.get(cache, key)
console.log("getResp", getResp);
const deleteResp = await client.delete(cache, key);
console.log("deleteResp", deleteResp);
// deleting a value from cache
return new Response(JSON.stringify({ response: getResp }));
},
};
さていよいよdeployです。
npm run deploy
を実行します。デプロイが完了したらURLが表示されますのでブラウザでアクセスします。Exception 101 エラーなどが表示されていなければ完了です。
ちょっとだけ改造
これだけだと何が起きているかわかりません。default
関数を見ると
const key = "key";
const value = "value";
const setResp = await client.set(cache, key, value);
console.log("setResp", setResp);
// getting a value from cache
const getResp = await client.get(cache, key)
console.log("getResp", getResp);
const deleteResp = await client.delete(cache, key);
console.log("deleteResp", deleteResp);
key
というキーを持つアイテムの値をvalue
として書き込み(`set)、読み込み(get)、削除(delete)しているようです。
以下に改造します。
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const client = new MomentoFetcher(env.MOMENTO_AUTH_TOKEN, env.MOMENTO_REST_ENDPOINT);
const cache = env.MOMENTO_CACHE_NAME;
const url = new URL(request.url);
const params = new URLSearchParams(url.search);
const key = params.get('key');
console.log(key);
const value = params.get('value');
console.log(value);
// setting a value into cache
const setResp = await client.set(cache, key, value);
console.log("setResp", setResp);
// getting a value from cache
const getResp = await client.get(cache, key)
console.log("getResp", getResp);
//const deleteResp = await client.delete(cache, key);
//console.log("deleteResp", deleteResp);
// deleting a value from cache
return new Response(JSON.stringify({ response: getResp }));
},
};
こうすることでURLパラメータからCacheに書き込みたいキーと値を設定でき、なおかつdelete
をさせないようにします。
https://<皆さん専用URL>/?key=demo1&value=demo1
で読み込むとdemo1というキーを持ったdemo1という値がCacheに書き込まれます。Momentoの管理者画面では以下のように値が確認できます。
もう少し改造してみる
Workersは500を超えるCloudflareのエッジで動作します。このため、Cacheへのデータ書き込みはマルチマスターになり当然結果整合性モデルになります。このため個別のセッションのユーザーリクエストごとに値が重複しないように工夫する必要があります。例えば上の例でいえば
https://<皆さん専用URL>/?key=demo1+<sessionid>&value=demo1
みたいな形にすることでユーザー固有のキーをとして処理することが可能です。
このパターンだと、すでになにがしかのセッションIDが個別ユーザーリクエストに基づいて付与されていることが前提になります。
では逆にリクエスト単位でユーザー毎のセッションIDを付与して、レスポンスでその値を戻す様にさらにスクリプトを改造してみます。
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const client = new MomentoFetcher(env.MOMENTO_AUTH_TOKEN, env.MOMENTO_REST_ENDPOINT);
const cache = env.MOMENTO_CACHE_NAME;
const url = new URL(request.url);
const params = new URLSearchParams(url.search);
const makeRandomString = (length: number) => Math.random().toString(36).substring(2, length + 2);
const key = makeRandomString(10);
console.log(key);
const value = params.get('value');
console.log(value);
// setting a value into cache
const setResp = await client.set(cache, key, value);
console.log("setResp", setResp);
// getting a value from cache
const getResp = await client.get(cache, key)
console.log("getResp", getResp);
//const deleteResp = await client.delete(cache, key);
//console.log("deleteResp", deleteResp);
// deleting a value from cache
return new Response(key);
},
};
こうすることで、毎回リクエスト固有のKeyのCacheが生成されます。
時間に余裕があればもう一つのWeb-SDK
もやってみたいと思います。
Discussion