Cloudflare Durable Object 入門
この記事では何回かに分けて分散型オブジェクトストレージであるDurable Objectを触っていきたいと思います。
Durable Object とは
1.データの保存場所
Durable Object とはCloudflareが提供する分散型のオブジェクトストレージです。オブジェクトストレージといえばAmazon S3が有名ですが、それといろいろな部分の考え方が異なります。まず一番わかりやすいデータの持ち方ですが、Cloudflareにはリージョンという概念が原則存在していません。このため、S3のような中央のストレージが存在していません。(それが必要な場合はAmazon S3互換オブジェクトストレージであるR2というサービスが存在します)
ではどういうデータの持ち方をするかといえば、あるオブジェクトを作成するリクエストが特定のエッジに着弾した場合、そのエッジでオブジェクトが作られます。このオブジェクトは追加の指示を与えない限り移動することはありません。そのオブジェクトには専用固有IDが割り振られます。次に当該オブジェクトへの呼び出しが来た場合、そのリクエストがどこのエッジに着弾したとしても、オブジェクトが存在しているエッジまでルーティングされそこで処理されます。そのため分散ストレージであるにもかかわらず、強い一貫性を常に提供するのがポイントです。
2.操作クライアント
Durable Objectを操作できるクライアントは2種類存在します。Workers と Durable Object です。WorkersはJavaScriptなどの実行基盤なのでわかりやすいと思いますが、Durable Object から Durable Object を呼び出せるのは大分特殊な考え方です。Durable Object というのは実は永続ストレージを持つWorkersで構成されています。このためストレージとしても機能するし、プログラム実行基盤としても使えますので、オブジェクトが別のオブジェクトを操作するということが可能となります。通常Workersはステートレスで動作しますが、永続ストレージによりステートフルな動作を実現させます。
余談ですが、先日リリースされたWorkers AI が呼び出すWebGPUはハードウェアのGPUを仮想化させたのち、Workers→Durable Object→WebGPUとして呼び出しています。
このため、Durable Object を操作する場合、まずはクラスとして宣言し、そのインスタンスを作成し、IDを割り振ることで個別オブジェクトへのアクセスが可能となります。
さっそくやってみる
ではGitHubにあるサンプルの永続カウンターをやってみます。
git clone git@github.com:cloudflare/durable-objects-template.git
cd durable-objects-template
プロジェクトがサンプルコード付きで展開されていますのでそのままDeployします。
wrangler deploy
DeployされたWorkersにアクセスをする際に/increment
を付与すると画面の数字は増えていきます。反対にdecrement
を付与すると数字は減っていきます。
通常のWorkersはステートレスであり、これを実現させるためにはKVなどのストレージを用いる必要がありますが、それを実現しているのがDurable Objectです。つまり永続性のあるWorkersを作る仕組み、とも言えます。
中身をみてみる
少しコードを見ていきましょう。
export default {
async fetch(request, env) {
return await handleRequest(request, env);
}
}
Workersにアクセスすると最初に呼び出される部分です。handleRequest
を呼び出しています。
async function handleRequest(request, env) {
let id = env.COUNTER.idFromName("A");
let obj = env.COUNTER.get(id);
let resp = await obj.fetch(request.url);
let count = await resp.text();
return new Response("Durable Object 'A' count: " + count);
}
A
という名前のDurable Objectを生成しています。なければ自動で作成します。env.COUNTER
はwrangler.toml
で以下のように定義されている環境変数です。
[durable_objects]
bindings = [{name = "COUNTER", class_name = "Counter"}]
ここでDurable Objectをクラスとして宣言可能としています。実際の宣言部分は以下です。
export class Counter {
constructor(state, env) {
this.state = state;
}
上記コードの以下がポイントです。
let resp = await obj.fetch(request.url);
でDurable Objectを呼び出します。その際以下の部分が実行されます。
async fetch(request) {
// Apply requested action.
let url = new URL(request.url);
<snip>
つまり通常のWorkersのサンプルと異なり2つfetch
が存在しています。1つ目はWorkersが呼び出されたときのもの。2つ目はWorkersがDurable ObjectにFetchを行った際に予備されるものです。
Workersの中にWorkersが動いている、という構成をこの仕組みで実現しています。
2つ目のfetch
における以下のコードはDurable Objectの永続性ストレージに値を書き込んでいます。
await this.state.storage.put("value", value);
Discussion