SvelteKit × Cloudflare Workers(Pages Functions)でローカル環境でCloudflare KVを利用する

## はじめに

ちょっとしたBotアプリ[^1]を実装したくなり、その際にSveltKitで実装することにした。そしてDeploy先の環境としては、色々アクセス制御等が楽なCloudflare Pagesでやってみることを考えた（[@sveltejs/adapter-cloudflare](https://kit.svelte.jp/docs/adapter-cloudflare)というCloudflare環境向けのビルドツールがあることもその理由）。
[^1]: 質問・回答のセットと、その質問・回答からタグを作ったものをDBに保存しておき、ユーザーからの質問からタグを生成し、そのタグの一致度が高いものを回答の候補にする。その回答の候補からユーザーへの質問に最もよいと思えるものをChatGPTに聞き、ChatGPTの応答をBotの応答として返す。

今回はそのアプリを実装する中で、KVSであるCloudflare KVを利用したくなった際に、Deployする前のローカルの開発環境で本番のKVと同じように開発をする方法はないものかと思い、調査してみたので、その備忘録を残そうと思う。

ライブラリなどのバージョンは以下。

- "@sveltejs/adapter-cloudflare": "^2.3.3",
- "@sveltejs/kit": "^1.27.1",
- "svelte": "^4.2.2",
- "wrangler": "^3.15.0"
- Node.js：v18.17.0

## Cloudflare WorkersからKVを利用する方法について

SveltKitでの話の前に、まずは、Cloudflare Workersとは？について触れ、Wokerの実装がどのようなものなのか？を簡単に見ていく。次に、そのWokerからCloudflareの[他のリソース](https://developers.cloudflare.com/workers/#related-products)（KVやD1など）を利用する方法を確認する。

### Cloudflare Workersとは？

高パフォーマンスなサーバーレス実行環境（エッジコンピューティング環境）で、その環境ではJavaScript(V8)を実行することが可能なので、このサービスを利用するとサーバーレスアプリケーション構築することができる。

図は[公式](https://www.cloudflare.com/ja-jp/learning/serverless/serverless-javascript/)のものがわかりやすいと思うが、CloudflareのCDNのエッジでJavaScriptが実行される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/ced78d35-a4f8-93c9-0452-a58e0696e076.png)

※ただし注意が必要で、あくまでJavaScriptなのでNode.jsのモジュールはそのまま利用できない。つまり、Node.jsの標準モジュールに依存する実装は動かない。動かすには[Node.js compatibility](https://developers.cloudflare.com/workers/runtime-apis/nodejs/#nodejs-compatibility)に書かれているような対応が必要。SveltKitの`@sveltejs/adapter-cloudflare`ではビルドエラーになる（内部的にはesbuildの[platform: 'browser'](https://github.com/sveltejs/kit/issues/2850#issuecomment-979941532)の設定になっているため。

### 基本的なWokerの実装について

[Get started guide](https://developers.cloudflare.com/workers/get-started/guide/)に書かれている通りにやってみると簡単にWorkers（サーバーレスのアプリ）を開発できる。

[`create-cloudflare`](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare)にはいくつかのテンプレートがあるが、[hello-world](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare/templates/hello-world/js)を見ると以下のようなコードになっている。

```js:hello-world
export default {
	async fetch(request, env, ctx) {
		return new Response('Hello World!');
	},
};
```

※他にも[`openapi`](https://github.com/cloudflare/workers-sdk/tree/main/packages/create-cloudflare/templates/openapi/ts)など色々なテンプレートがあるみたい。

### WorkerからのCloudflare KVの呼び出し方法

WorkerからCloudflareの各リソース（[KV](https://developers.cloudflare.com/kv/)や[D1](https://developers.cloudflare.com/d1/)など）を利用するには[Bindings](https://developers.cloudflare.com/workers/configuration/bindings/)に書かれているように、Wokerに対してリソースを紐づける設定が必要になる。

具体的には`wrangler.toml`というファイルに以下のような設定を行う（詳細は[KV namespaces](https://developers.cloudflare.com/workers/wrangler/configuration/#kv-namespaces)を参照）。

```toml:wrangler.toml
kv_namespaces = [
  { binding = "<TEST_NAMESPACE>", id = "<TEST_ID>" }
]
```

上記のように設定することで、Worker内では以下のように`env.TEST_NAMESPACE.put()`などのKV操作APIを実行できるようになる（詳細は[Interact with your KV namespace](https://developers.cloudflare.com/kv/get-started/#interact-with-your-kv-namespace-via-a-worker)を参照）。

```js
export default {
	async fetch(request, env, ctx) {
        await env.TEST_NAMESPACE.put("KEY", "VALUE");        
        const value = await env.TEST_NAMESPACE.get("KEY");        
        if (value === null) {            
            return new Response("Value not found", { status: 404 });        
        }        
        return new Response(value);    
	}, 	
};
```

- 参考：[KV namespace bindings](https://developers.cloudflare.com/workers/configuration/bindings/#kv-namespace-bindings)

## SveltKitでのKVの利用方法

SveltKitの場合、上記の[WorkerからのCloudflare KVの呼び出し方法](#workerからのcloudflare-kvの呼び出し方法)でみたBindingsが少し違っていて、[Bindings](https://kit.svelte.jp/docs/adapter-cloudflare#bindings)に書かれている通り、[`platform`プロパティ](https://kit.svelte.dev/docs/adapters#platform-specific-context)からKVにアクセスできるようにしてくれるので、以下のような実装ができる（`@sveltejs/adapter-cloudflare`を利用する場合）。

```js
import { json } from '@sveltejs/kit';

export async function GET({ platform }) {
	await platform.env.first_kv.put('key', JSON.stringify({ value: 'value' }));
	const x = await platform.env.first_kv.get('key');
	return json({ x }, { status: 200 });
}
```

## ローカル環境でKVにアクセスする

方法としては[wrangler](https://developers.cloudflare.com/workers/wrangler/)の[pages dev](https://developers.cloudflare.com/workers/wrangler/commands/#dev-1)コマンドを利用する。具体的には、ローカル環境なのでダミーでいいので`wrangler.toml`を以下のように定義する。

```toml:wrangler.toml
kv_namespaces = [
    { binding = "first_kv", id = "hoge" }
]
```

あとは、`vite build`でビルドを行い、以下の`pages dev`コマンドでCloudflare Pagesをエミュレートすればいい。すると、以下のようにSveltKitで実装したものがちゃんとSSRで描画され、またサーバーへのリクエストがログに出ていることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/62469f55-9870-7a07-cf05-594fa170d253.png)

今回、以下のような画面でボタンをクリックして`GET:/api/kv-test`を呼び出すような実装をしていたが、上記のログからと以下のスクショからうまく動いていることが確認できる。

| ボタンクリック前 | クリック後 |
| ---- | ---- |
| ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/ec923027-0861-d966-b96f-201d69d170b2.png) | ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/5769892f-4cde-8e66-e325-1f2e047a8cbf.png) |

上記のような方法であれば、実際のCloudflare KV上にアクセスすることなく、アプリケーションのテストなどを行うことができるだろう。

ちなみに、いったいどこにデータがあるのか？だが、`.wrangler/state/v3/kv/hoge/blobs`以下にファイルとして存在しているみたい。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/3455ae50-0774-be0d-4514-feedd89bfefb.png)



## おまけ
### Cloudflare KVを利用した経緯（Cloudflare WorkerではNode.jsの標準モジュールは利用できないので注意）

実はKVを利用したかったわけではなく、KVSとしてはRedisなどのリッチなものを利用したかった。Redisのサーバーレス版としては[Upstash](https://upstash.com/docs/redis/overall/getstarted)などがあったりするのでそれを利用しようと考えていたが、その時にREST APIがあることを忘れており、[ioredis](https://github.com/redis/ioredis)などのNode.jsライブラリを利用しようとしていた。。。

そのため、`@sveltejs/adapter-cloudflare`を利用してビルドすると[Cloudflare adapter: Could not resolve "path" and "fs"](https://github.com/sveltejs/kit/issues/2850)にあるようなエラーが発生し、それならCloudflareにあるKVを使ってみるか、というわけで利用してみた。

### wranglerの初回インストールについて

一番最初に`wrangler`のコマンドを実行する際に、ログイン（コマンドラインからのCloudflareへのアクセス認可）を求められる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/a30dd79a-14ab-3c77-9b34-975a9a044640.png)

OAuth2.0の認可フローになっているので、認可するとコールバックで以下のようなリンクにとび、認可完了画面が表示される。
```
http://localhost:8976/oauth/callback?code=5ntORsEyBHMl0V8sZx9Dpgwktu0WbS6pqulLK6kgTn4.F95HOJ8ggrH1uBOIKDv72FHcpmnrJAYW91s1CKCTF8o&scope=account%3Aread%20user%3Aread%20workers%3Awrite%20workers_kv%3Awrite%20workers_routes%3Awrite%20workers_scripts%3Awrite%20workers_tail%3Aread%20d1%3Awrite%20pages%3Awrite%20zone%3Aread%20ssl_certs%3Awrite%20constellation%3Awrite%20ai%3Aread%20offline_access&state=rFdbAdFV~~JHvV4LxQkHbeMSBBX~VpR2
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/455afa67-3b2c-9f6d-5959-3574dfc3670d.png)

### 本番環境でのバインディングの設定方法

`wrangler`コマンドからも同じことはできると思うが、GUIでやるなら以下から設定できる。

※注意点として、バインディングの設定は次のDeployから反映されるので、すでにDeploy済みのWorkerには反映されない。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/0604424f-4980-4015-0528-882cdcf66d96.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/61a58ad7-ac0e-f3ce-8683-e98905cea9ae.png)

ちなみに、Worker（Pages Function）のログはDeployの詳細ページから確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/c0b701b3-88d3-cbcc-1129-fa7b34ae9efe.png)

### SveltKitのHMR（Hot Module Replacement）とセットで`wrangler pages dev`を実行するのはできない？！

`wrangler pages dev`でローカルの開発環境でCloudflare KVをエミュレートして開発はできるが、1点困ったことに`vite dev`では`@sveltejs/adapter-cloudflare`を利用したビルドはされないので、`vite build`を行う必要がある。となると、毎回ファイルの変更ごとにビルドし直しになるので面倒…ということがある。

この問題を解決するには、[nodemon](https://www.npmjs.com/package/nodemon)を利用できる。以下のようにnodemonでビルドコマンドを実行することで、src以下の変更を検知して再度ビルドを走らせてくれる。

```console
$ npx nodemon --watch src --exec 'yarn build'
```

`wrangler pages dev`のコマンドも同様に、できるかな～と思ったがうまくいかなかった…。この辺り、解決する方法はあるのだろうか…。過去に同じような話をしている[issue](https://github.com/sveltejs/kit/issues/2966)は見つけたが、解決はしていなさそう…。

うまくいかなかったコマンドは以下。

```console
$ npx nodemon --watch .svelte-kit/cloudflare --exec 'npx wrangler pages dev .svelte-kit/cloudflare'
$ npx nodemon --watch .svelte-kit/output --exec 'npx wrangler pages dev .svelte-kit/cloudflare'
$ npx nodemon --watch src --exec 'npx wrangler pages dev .svelte-kit/cloudflare'
```

※`vite build --watch`というコマンドもあるので、SveltKitのビルドの方はnodemonでやる必要はないかも。