📖

Cloudflare Workers からMomento Cacheを呼び出す

2023/09/06に公開

先日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の環境をセットアップします。手順は自社サービスではないので割愛しますがここにあります。
https://docs.momentohq.com/ja/getting-started

なんと日本語手順がありました。初めて見たときの感想は(ふふん、やるじゃないの)とった感じです。Cacheの名前はdemo-cacheにしておいて下さい。

Workers 側の作業

ではここからWorkers側の作業です。手順は以下のMomentoが管理しているGitHubにあります。
https://github.com/momentohq/client-sdk-javascript/tree/main/examples/cloudflare-workers

HTTP-APIWEB-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を以下で置換します。

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