名前付きカウンターにリクエストをルーティングする

CloudflareBeginner
オンラインで実践に進む

はじめに

通常の Cloudflare Worker は多くのリクエストに応答できますが、あるリクエストの次のリクエストが同じ実行中の JavaScript インスタンスに到達するとは限りません。このステートレスな設計は、リクエストごとに独立した処理には適しています。一方、サポートキューの待機人数のように、複数のリクエストで変化する 1 つの値を共有する必要がある場合は扱いにくくなります。

Durable Object は、アプリケーションにアドレス指定可能な調整単位を提供します。この実験では、カウンター名ごとに異なるオブジェクトを選択します。support へのリクエストは常に同じ論理カウンターに到達し、billing へのリクエストは別のカウンターに到達して別々の状態を持ちます。Cloudflare が基盤となるランタイムを移動または再起動しても、安定したオブジェクトのアイデンティティと SQLite に保存された状態は、アプリケーションの契約として維持されます。

この実験では、次の 4 つの概念を組み合わせます。

  1. class は、1 つのカウンターオブジェクトが実行できる処理を定義します。
  2. namespace は、その class によって管理されるオブジェクトの集合です。
  3. binding は、入口となる Worker から namespace にアクセスするための名前を提供します。
  4. getByName() は、同じ検証済みの名前から同じオブジェクト参照を取得し、RPC メソッドでそのオブジェクトのコードを呼び出します。

アプリケーションを構築し、名前に基づくルーティングをローカルで確認します。その後、自分の Cloudflare 学習用アカウントにデプロイし、ターミナルの結果を Dashboard で確認します。最後に、class の namespace と Worker を削除します。

このコースを始める前に、LabEx を Cloudflare アカウントに接続するを完了してください。 この実験では、LabEx VM のターミナル、Wrangler のデバイス認証、アカウント確認、アカウント ID の設定を学びます。小規模な JavaScript Worker が HTTP リクエストを処理する方法をすでに理解していることを前提とします。Durable Objects に関する予備知識は必要ありません。

現在の公式ドキュメントでは、SQLite を使用する Durable Objects は Workers Free で利用できます。この実験では、削除可能な class namespace を 1 つ、少数の小さなオブジェクト、上限のあるリクエストだけを作成します。Workers Paid は必要ありません。セットアップでは、Node.js 22.22.0 とプロジェクトローカルの Wrangler 4.132.0 を /home/labex/project/named-counters にインストールします。ログイン、クラウド上のリソース作成、コードのデプロイ、学習者による実装の完了は行いません。

VM を認証してアプリケーション名を付ける

このステップでは、新しい LabEx VM を Cloudflare の学習用アカウントに接続し、一意のアプリケーション設定を作成します。ブラウザーで Dashboard にサインインしていても、新しい VM 内のコマンドが自動的に認証されるわけではありません。

用意されたプロジェクトに移動し、固定された Wrangler のバージョンを確認します。

cd /home/labex/project/named-counters
npx wrangler --version

4.132.0 と表示されることを確認します。Wrangler のデバイス認証フローを開始します。

npx wrangler login --device --browser=false

Wrangler は URL と短いデバイスコードを表示します。ブラウザーで URL を開き、コードを入力して、選択されたアカウントが専用の学習用アカウントであることを確認します。認証する前に、要求された権限も確認してください。Wrangler はターミナルに戻った後も処理を続ける必要があるため、バックグラウンドアクセスが表示される場合があります。パスワードやトークンをターミナル経由で送信しないでください。

ブラウザーに成功が表示されたら、ターミナルに戻り、Wrangler の処理が完了するまで待ちます。アカウント情報を構造化形式で取得します。

npx wrangler whoami --json

loggedIn: true であることを確認し、目的のアカウントを特定します。アカウントが 1 つしか表示されない場合も確認してください。アカウント名は人間が確認するための情報で、ID は安定した設定値です。ターミナルに ID を表示する必要はありません。

構造化された結果を保存し、機密情報を含まないアカウント名だけを表示して、LabEx Learning に一致する ID を選択します。

WHOAMI="$(npx wrangler whoami --json)"
printf '%s\n' "$WHOAMI" | jq '{loggedIn, authType, accounts: [.accounts[] | {name}]}'
ACCOUNT_ID="$(printf '%s\n' "$WHOAMI" | jq -r '.accounts[] | select(.name == "LabEx Learning") | .id')"
test -n "$ACCOUNT_ID"

$(...) はコマンドの出力をシェル変数に格納します。jq は最初に確認用としてアカウント名だけを表示し、その後、対応する ID を非表示のまま選択します。test -n は、選択された値が空でない場合にだけ成功します。専用の学習用アカウントの表示名が異なる場合は、その名前を確認してから、選択式の LabEx Learning を実際の名前に置き換えてください。

一意の Worker 名を生成します。openssl rand -hex 6 は 12 個のランダムな 16 進文字を生成し、$(...) はその値をシェル変数に埋め込みます。

RUN="labex-c10-o01-$(openssl rand -hex 6)"
printf '%s\n' "$RUN"

wrangler.jsonc を作成します。設定ファイルは、デプロイするコードと、実行時に接続する Cloudflare の機能を Wrangler に伝えます。引用符で囲まれていない JSON マーカーによって $RUN$ACCOUNT_ID が展開され、バックスラッシュによって $schema キーはそのまま保持されます。

cat > wrangler.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/index.js",
  "compatibility_date": "2026-09-18",
  "workers_dev": true,
  "preview_urls": false,
  "observability": {
    "enabled": true,
    "head_sampling_rate": 1
  },
  "durable_objects": {
    "bindings": [
      { "name": "COUNTERS", "class_name": "Counter" }
    ]
  },
  "exports": {
    "Counter": { "type": "durable-object", "storage": "sqlite" }
  }
}
JSON

このファイルはアプリケーションを記述するだけで、まだ Cloudflare 上には何も作成しません。observability は、後の Dashboard 確認で使用するリクエストログとアプリケーションログを保持します。Durable Object に関するフィールドの役割は次のステップで確認します。

Namespace、Binding、Class を接続する

このステップでは、Durable Objects の設定を、リクエストがどのように 1 つのステートフルなオブジェクトへ到達するかを示す対応関係として読み取ります。その後、binding をコードから利用できるように、実行時の型情報を生成します。

Durable Object class は、1 つのオブジェクトに対する JavaScript の設計図です。後で作成する Counter class は、値のインクリメントや読み取りなどの操作を定義します。

namespace は、その class によって管理されるすべてのオブジェクトの集合です。1 つの namespace に、supportbilling、その他多数の名前付きカウンターを含めることができます。namespace が意味するのはカウンター同士が 1 つの値を共有することではありません。安定した各オブジェクトのアイデンティティが、それぞれ別のストレージを所有します。

binding は、入口となる Worker が namespace にアクセスするために使用する名前です。この設定では、COUNTERS という名前を Counter class にバインドします。そのため、コードでは env.COUNTERS を使用します。

exports エントリは、class の現在のライフサイクル状態を宣言します。初回デプロイ時に、Cloudflare が SQLite ストレージバックエンドを使用して Counter を作成することを示します。SQLite は新しい class に推奨されるバックエンドで、Workers Free で利用できます。この実験の小さなテーブルは、各オブジェクト内に整数を 1 つだけ保存します。

設定から型情報を生成します。

npx wrangler types

生成されたファイルから COUNTERS を検索します。

grep -n 'COUNTERS' worker-configuration.d.ts

次のような行が表示されます。

COUNTERS: DurableObjectNamespace<import("./src/index").Counter>;

生成された周辺のテキストは変わることがありますが、重要なのは次の 3 点です。binding 名が COUNTERS であること、型が DurableObjectNamespace であること、エクスポートされた Counter class を参照していることです。binding を変更した場合は、設定とコードが気付かないうちにずれないよう、必ず型情報を再生成してください。

名前付きカウンターを構築する

このステップでは、Counter class と入口となる Worker を実装します。Worker は、検証済みの URL 名を 1 つのオブジェクトへルーティングします。

すべての Durable Object にはプライベートストレージがあります。コンストラクターは counter_state という 1 行テーブルを作成し、その行がまだ存在しない場合にだけ初期値を挿入します。blockConcurrencyWhile() は、この短い初期化が終わるまでオブジェクトへのリクエストを待機させます。スキーマの設定には適していますが、すべてのリクエストや外部ネットワーク処理をこの中に入れてはいけません。

公開される increment()getCount()RPC メソッドです。RPC は remote procedure call(リモートプロシージャコール)の略で、Worker から Durable Object のスタブ上にあるメソッドを、非同期 JavaScript オブジェクトのメソッドであるかのように呼び出せます。Cloudflare はその呼び出しを選択されたオブジェクトへ転送します。

Worker のエントリーポイントを作成します。

cat > src/index.js <<'JS'
import { DurableObject } from "cloudflare:workers";

export class Counter extends DurableObject {
  constructor(ctx, env) {
    super(ctx, env);
    ctx.blockConcurrencyWhile(async () => {
      this.ctx.storage.sql.exec(`
        CREATE TABLE IF NOT EXISTS counter_state (
          key INTEGER PRIMARY KEY CHECK (key = 1),
          value INTEGER NOT NULL
        )
      `);
      this.ctx.storage.sql.exec(
        "INSERT OR IGNORE INTO counter_state (key, value) VALUES (1, 0)"
      );
    });
  }

  increment() {
    return this.ctx.storage.sql
      .exec("UPDATE counter_state SET value = value + 1 WHERE key = 1 RETURNING value")
      .one().value;
  }

  getCount() {
    return this.ctx.storage.sql
      .exec("SELECT value FROM counter_state WHERE key = 1")
      .one().value;
  }
}

function json(data, status = 200) {
  return Response.json(data, { status });
}

function counterName(pathname) {
  const match = pathname.match(/^\/counters\/([^/]+)$/);
  if (!match) return { error: "not_found", status: 404 };

  let name;
  try {
    name = decodeURIComponent(match[1]);
  } catch {
    return { error: "invalid_counter_name", status: 400 };
  }

  if (!/^[a-z][a-z0-9-]{0,31}$/.test(name)) {
    return { error: "invalid_counter_name", status: 400 };
  }
  return { name };
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (request.method === "GET" && url.pathname === "/health") {
      return json({ status: "ok" });
    }

    const parsed = counterName(url.pathname);
    if (parsed.error) return json({ error: parsed.error }, parsed.status);
    if (request.method !== "GET" && request.method !== "POST") {
      return json({ error: "method_not_allowed" }, 405);
    }

    const name = parsed.name;
    const stub = env.COUNTERS.getByName(name);
    const count = request.method === "POST"
      ? await stub.increment()
      : await stub.getCount();

    console.log(JSON.stringify({
      event: request.method === "POST" ? "counter_incremented" : "counter_read",
      name,
      count
    }));
    return json({ name, count });
  }
};
JS

ルーティングで使用する getByName(name) が、アイデンティティの境界になります。同じ検証済みテキストは常に同じ論理オブジェクトを決定的に選択し、異なるテキストは別のオブジェクトを選択します。スタブは参照にすぎません。RPC 呼び出しが実際にオブジェクトへ到達した時点で、オブジェクトは遅延作成されます。

用意されている決定的なテストを実行します。テストは小さな namespace の fixture を使用するため、クラウドへのリクエストは発生しません。

NODE_NO_WARNINGS=1 node --experimental-loader ./test/cloudflare-loader.mjs --test test/worker.test.mjs

この小さな loader は、Node がモジュールをインポートできるように、cloudflare:workers の基底 class のローカルな代替実装だけを提供します。namespace の fixture がテスト対象のすべての呼び出しを制御するため、Cloudflare API には接続しません。3 件のテストが成功することを確認します。その後、デプロイせずに Wrangler で Worker をビルドします。

npx wrangler deploy --dry-run

テストは HTTP ルーティングの契約を検証し、dry run は Wrangler が実際の Durable Object class をバンドルできることを検証します。どちらの操作でも、リモート namespace は作成されません。

ローカルで安定した名前を確認する

このステップでは、ローカルの Workers ランタイムでアプリケーションを実行します。クラウドリソースを作成する前に、2 つの名前を使ってルーティング規則を確認します。

Wrangler をポート 8787 でバックグラウンド起動します。> はログを保存し、2>&1 はエラーを通常の出力と結合し、& はターミナルのプロンプトをすぐに返します。$! は、直前に起動したコマンドのプロセス ID です。

npx wrangler dev --port 8787 > .labex/dev.log 2>&1 &
echo $! > .labex/dev.pid

ヘルスルートが応答するまで待ちます。ループは 1 秒ごとに 1 回試行し、Worker が応答するとすぐに停止します。

for attempt in $(seq 1 30); do
  if curl --silent --fail http://127.0.0.1:8787/health; then
    break
  fi
  sleep 1
done

{"status":"ok"} と表示されることを確認します。support カウンターを 2 回インクリメントします。

curl --silent --request POST http://127.0.0.1:8787/counters/support | jq
curl --silent --request POST http://127.0.0.1:8787/counters/support | jq

レスポンスで、support1 から 2 へ変化することを確認できます。

{
  "name": "support",
  "count": 2
}

次に、billing を 1 回インクリメントします。

curl --silent --request POST http://127.0.0.1:8787/counters/billing | jq

値は 3 ではなく 1 になります。namespace は集合であり、各名前はその集合内の分離されたオブジェクトを選択します。

値を変更せずに、両方のオブジェクトを読み取ります。

curl --silent http://127.0.0.1:8787/counters/support | jq
curl --silent http://127.0.0.1:8787/counters/billing | jq

カウントが 21 のままであることを確認します。最後に、getByName() がオブジェクトを選択する前に、不正な入力が拒否されることを確認します。

curl --silent --request POST --write-out '\nHTTP %{http_code}\n' \
  http://127.0.0.1:8787/counters/Not_Allowed

{"error":"invalid_counter_name"} と HTTP 400 が表示されることを確認します。アンダースコアと大文字は、定義された名前の規則に含まれていません。

Namespace をデプロイして確認する

このステップでは、ローカルランタイムを停止し、同じアプリケーションを Cloudflare にデプロイします。その後、API の動作を Dashboard に表示される namespace、binding、メトリクス、ログと結び付けて確認します。

保存した ID を使って、開発プロセスだけを停止します。

kill "$(cat .labex/dev.pid)"
wait "$(cat .labex/dev.pid)" 2>/dev/null || true

Worker と、宣言した SQLite 対応の Counter class をデプロイします。

npx wrangler deploy

Wrangler は公開 workers.dev URL と class の調整結果を表示します。例の値を置き換えて、正確な URL を保存します。

WORKER_URL="https://YOUR_WORKER_URL"

エッジのルートが利用可能になるまで、少し時間がかかることがあります。Durable Object にアクセスしないヘルスルートだけをポーリングします。

for attempt in $(seq 1 30); do
  if curl --silent --fail "$WORKER_URL/health"; then
    break
  fi
  sleep 2
done

support に 2 回、billing に 1 回リクエストします。

curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/support" | jq
curl --silent --request POST "$WORKER_URL/counters/billing" | jq

値を読み取ります。

curl --silent "$WORKER_URL/counters/support" | jq
curl --silent "$WORKER_URL/counters/billing" | jq

リモートアプリケーションでも、ローカルランタイムと同じアイデンティティの契約が確認できるはずです。support2billing1 になります。

Cloudflare Dashboard で Workers & Pages を開きます。一意の名前を付けた Worker がアプリケーション一覧に表示されます。次のスクリーンショットにある Worker 名、タイムスタンプ、アカウント全体の使用量は、テスト実行時の例です。自分のターミナルで生成された labex-c10-o01-... という名前を探してください。

Workers and Pages のアプリケーション一覧に表示されたデプロイ済みの lab Worker

Cloudflare Dashboard を開き、Workers & Pages → Overview → 自分の labex-c10-o01-... Worker → Settings → Bindings に移動します。COUNTERS という名前の Durable Object binding と、その Counter class を探します。Worker は binding 名を認識し、Cloudflare はそれを class export で宣言された namespace に接続します。

binding の図で、Worker が COUNTERS を通じて Durable Object に接続されていることを確認します。このスクリーンショットにある Worker 名と namespace 名は実行ごとに異なる例です。重要なのは binding 名と接続関係です。

Worker に接続された COUNTERS Durable Object binding

次に、Developer Platform のナビゲーションから Durable Objects を開きます。削除対象の Worker が所有する namespace を選択します。ストレージに SQLite が使用され、class が Counter であることを確認します。namespace は class レベルの集合です。その中のオブジェクトを supportbilling という名前で識別します。

namespace の概要に Storage: SQL と表示されます。namespace 名と ID はテスト実行ごとに異なるため、自分の値とは一致しません。

SQL ストレージを使用する Counter namespace の概要

namespace の Metrics ビューを開きます。最近のリクエストが表示されるまで時間がかかる場合があるため、一時的にグラフが空でも判断できません。グラフを表示させるために、大量のリクエストループを実行しないでください。

例の namespace のスクリーンショットでは、ランタイムへのリクエストが成功しているにもかかわらず、最近の呼び出し数が 0 のままです。これは、Dashboard のメトリクスが遅延するため、機能確認の決定的な証拠ではなく補助的な情報であることを示しています。

Worker に戻り、Observability → Logs を開きます。最近の counter_incremented または counter_read イベントを探します。構造化ログには合成したカウンター名とカウントが含まれますが、アカウント識別子や認証情報は含まれません。上で実行した上限付きリクエストのいずれかと照合してください。

一致するイベントを 1 つ展開します。テスト実行では、検証用に生成された名前のカウントが 2 になり、イベントグラフでは成功したリクエストとエラー 0 件が報告されました。自分の合成名と合計値は異なります。

名前とカウントを含む構造化された counter_read イベント

Worker 名、オブジェクト ID、タイムスタンプ、リクエスト数などの Dashboard の値は、実行ごとに異なります。CLI、API、ランタイムによる確認が決定的な証拠であり、Dashboard の画面は同じ関係がどこで確認できるかを学ぶためのものです。

Namespace を削除してログアウトする

このステップでは、Counter class を意図的に廃止し、その namespace と保存データを削除します。その後、Worker を削除して、この VM の Wrangler セッションを無効化します。

Worker スクリプトだけを削除しても、保存された Durable Object データを削除するという明確な宣言にはなりません。exports のライフサイクルでは、deleted tombstone(削除済みトゥームストーン)を使用します。これは、1 つの class namespace を完全に削除するよう Cloudflare に伝える短期間の設定エントリです。この操作にはゴミ箱がないため、class と Worker 名がこの実験のものであることを確認してください。

Counter export を含まない最小限のクリーンアップ用エントリーポイントを作成します。

cat > src/cleanup.js <<'JS'
export default {
  fetch() {
    return Response.json({ status: "cleanup" }, { status: 410 });
  }
};
JS

クリーンアップ用の設定を作成します。同じ Worker 名とアカウントを維持し、binding を削除して、Counter だけを削除済みとしてマークします。

ACCOUNT_ID="$(node -e 'console.log(JSON.parse(require("fs").readFileSync("wrangler.jsonc", "utf8")).account_id)')"
cat > wrangler.cleanup.jsonc <<JSON
{
  "\$schema": "./node_modules/wrangler/config-schema.json",
  "name": "$RUN",
  "account_id": "$ACCOUNT_ID",
  "main": "src/cleanup.js",
  "compatibility_date": "2026-09-18",
  "workers_dev": true,
  "preview_urls": false,
  "exports": {
    "Counter": { "type": "durable-object", "state": "deleted" }
  }
}
JSON

トゥームストーンをデプロイします。

npx wrangler deploy --config wrangler.cleanup.jsonc

Wrangler の調整結果を注意深く読みます。Counter が削除されたと表示されるはずです。これにより、class namespace と、supportbilling、独立した検証用カウンターに保存された小さな値が完全に削除されます。

次に、残っているステートレスなクリーンアップ用 Worker を削除します。

npx wrangler delete --config wrangler.cleanup.jsonc

正確に labex-c10-o01-... のアプリケーションだけを確認します。Dashboard でその Worker が存在しないことと、その Worker が所有していた namespace が表示されなくなったことを確認してください。過去のメトリクスやログは一時的に残る場合がありますが、アクティブなリソースではありません。

認証を削除する前に、認証済みの削除確認を実行します。

python3 .labex/verify.py deleted

PASS: deleted と表示された後で、ログアウトします。

npx wrangler logout
npx wrangler whoami --json

最後の出力に loggedIn: false が明示されていることを確認します。ネットワークエラーはログアウトの証拠になりません。

まとめ

最初の Durable Objects アプリケーションを構築して運用しました。class が 1 つのオブジェクトの動作を定義し、namespace がその class のオブジェクトをまとめ、binding が namespace を Worker に公開し、getByName() が 1 つの論理オブジェクトを決定的に選択することを学びました。RPC メソッドで SQLite に保存された状態を変更・読み取り、同じ名前ではカウントを共有し、異なる名前では状態を分離しました。また、オブジェクトの選択前に不正な名前を拒否しました。

さらに、ランタイムの動作を Cloudflare Dashboard と結び付けて確認し、宣言的な class のトゥームストーンを使って Worker を削除する前に namespace とそのデータを削除しました。次の実験では、このアイデンティティモデルを基に、SQLite をアクティビティログとして扱い、永続ストレージが一時的なインメモリ状態とどのように異なるかを確認します。