はじめに
サポートアプリケーションで、ストレージバケットをパブリックにせずにドキュメントを受け取り、そのまま返せるようにします。プライベートな R2 バケットを Worker に接続し、サイズ制限付きのアップロードを実装して、呼び出し元へダウンロードをストリーミングします。ストリームはダウンロード全体を最初にメモリへ読み込むのではなく、利用可能になったチャンクを順次配信します。
最初に「Organize a Document Bucket」と Workers の設定およびシークレットに関するレッスンを完了してください。この新しい VM には Node.js 22.22.0、Wrangler 4.131.1、合成ドキュメント、提供済みの認証モジュールが含まれています。このモジュールは使い捨てトークンでデモ用エンドポイントを保護するため、このストレージレッスンで無制限のアップロードサービスが公開されることはありません。このコースの後半で、アプリケーションの認可を修正する方法を学びます。
開始する前に、学習用アカウントで R2 の有効なサブスクリプションを持ち、新しいバケットと Worker を管理する権限が必要です。R2 の料金を確認してください。ストレージとオペレーション、および Worker の利用量は別々に計測されます。購入済みのドメインは必要ありません。合成ファイルだけを使用し、最後にこの実験で作成した Worker、オブジェクト、バケットを削除してください。各 VM には個別の認証が必要で、以前の VM のリソースは再利用されません。
アプリケーションのバケットを接続する
このステップでは、この VM を認証し、アプリケーション専用のプライベートバケットを作成します。デバイス認証によって学習用アカウントを確認します。R2 バケットの管理には、そのアカウントだけに制限した別の API トークンを使用します。
以下のコマンドで使用する構文のために Bash を起動し、準備済みのプロジェクトへ移動してツールを確認します。リソース名の変数を利用できるように、同じターミナルを開いたままにしてください。
bash
cd /home/labex/project/r2-lab
export PATH="$PWD/.tools/node-v22.22.0-linux-x64/bin:$PATH"
node --version
npx wrangler --version
表示されたデバイスコードを自分のブラウザで認証します。同意する前に、学習用アカウントと、要求されているアカウントおよびユーザーの読み取りスコープを確認してください。
npx wrangler login --device --browser=false --scopes account:read user:read workers_scripts:write workers_kv:write
npx wrangler whoami --json
loggedIn: true であることを確認します。アカウントが 1 つだけ表示される場合でも、アカウント名を確認してください。以下の YOUR_ACCOUNT_ID を、そのアカウントの実際の 32 文字の ID に置き換えます。openssl rand -hex 6 は 12 個のランダムな 16 進文字を生成するため、この実験で以前の実行と名前が衝突することはありません。ヒアドキュメントによって標準の設定ファイルが作成され、シェルが変数をその中へ展開します。
ACCOUNT_ID=YOUR_ACCOUNT_ID
RUN_ID=$(openssl rand -hex 6)
NAME="labex-c05-r02-$RUN_ID"
BUCKET="$NAME-docs"
cat > wrangler.jsonc <<JSON
{"name":"$NAME","account_id":"$ACCOUNT_ID","main":"src/index.js","workers_dev":true,"compatibility_date":"2026-07-30","r2_buckets":[{"binding":"DOCUMENTS","bucket_name":"$BUCKET"}]}
JSON
バケットを管理するため、Cloudflare プロフィールの API Tokens ページを開き、この実験の名前を付けたカスタムトークンを作成します。Account → Workers R2 Storage → Edit を許可し、Account Resources を保存した学習用アカウントだけに制限します。有効期限は短く設定してください。他のアカウントや無関係な権限は含めないでください。この管理トークンは、作成や削除などのバケット管理に使用します。この実験では、Worker は DOCUMENTS バインディングを通じて R2 オブジェクトにアクセスします。
トークンを一度だけ、この非表示の VM プロンプトへ貼り付けます。umask 077 によりファイルを自分のユーザーだけが読めるようにし、read -s により入力を非表示にします。このファイルでは Wrangler の標準トークン変数を使用し、Git の対象外になります。
umask 077
read -r -s -p 'R2 management API token: ' R2_MANAGEMENT_TOKEN; printf '\n'
printf 'CLOUDFLARE_API_TOKEN=%s\n' "$R2_MANAGEMENT_TOKEN" > .env.management
unset R2_MANAGEMENT_TOKEN
R2 の管理コマンドでのみ --env-file=.env.management を使用してください。通常の whoami は引き続き VM のデバイス認証を確認します。
--env-file は各 Wrangler コマンドの末尾に置きます。これにより、ファイル引数のリストにコマンド名が取り込まれるのを防げます。各バケットの作成後、Wrangler が設定へのバインディング追加を尋ねたら、n を入力して Enter を押してください。必要なバインディングはすでに設定されています。
npx wrangler r2 bucket create "$BUCKET" --env-file=.env.management
バケット一覧を表示し、生成された正確な名前を探します。他のバケットは別の作業に属しているため、そのまま残してください。
npx wrangler r2 bucket list --env-file=.env.management
Dashboard で Storage & databases → R2 → Overview を開き、この正確なバケットを選択して、空のオブジェクト一覧を確認します。設定では、パブリック開発 URL とカスタムドメインを無効のままにします。Dashboard に表示されるバケット名で対象を確認できます。保存されたバイト列の確認は、後のダウンロードテストで行います。
Worker スクリプトの権限はデプロイを可能にします。KV の権限は Wrangler の削除記録を管理するために必要ですが、この実験では KV namespace を作成しません。R2 管理トークンは、アカウントにスコープされた別の認証情報として保持されます。
サイズ制限付きのアップロードとストリーミングダウンロードを実装する
このステップでは、DOCUMENTS 設定バインディングをオブジェクト操作に利用します。バインディングとは、Cloudflare が Worker に実行時オブジェクトとして提供するものです。env.DOCUMENTS は名前で設定されたプライベートバケットを参照するため、Worker が使用するために S3 シークレットは必要ありません。
提供されている src/auth.js は、使い捨ての bearer トークンを確認します。このルートでは、単純な .txt ドキュメント名だけを受け付けます。PUT は選択したキーのバイト列を置き換えます。この例では、長さヘッダーを省略するクライアントも含め、最大 1 MiB(1,048,576 バイト)まで許可します。アップロードのチャンクはこの上限までだけ収集するため、R2 は長さが既知のボディを受け取れます。ダウンロードでは object.body を直接レスポンスへ渡し、ストリーミングのまま処理します。
次のヒアドキュメントでハンドラーを作成します。
cat > src/index.js <<'JS'
import { authorized } from "./auth.js";
const MAX_BYTES = 1024 * 1024;
export default {
async fetch(request, env) {
const path = new URL(request.url).pathname;
if (path === "/health" && request.method === "GET") return new Response("ok");
if (!await authorized(request, env)) return new Response("Unauthorized", { status: 401 });
if (!/^\/documents\/[a-z0-9-]+\.txt$/.test(path)) return new Response("Not found", { status: 404 });
const key = path.slice(1);
if (request.method === "PUT") {
if (Number(request.headers.get("Content-Length")) > MAX_BYTES)
return new Response("Too large", { status: 413 });
// Count actual bytes too: a request may omit Content-Length.
const reader = request.body?.getReader();
if (!reader) return new Response("Body required", { status: 400 });
const chunks = [];
let total = 0;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
total += value.byteLength;
if (total > MAX_BYTES) {
await reader.cancel();
return new Response("Too large", { status: 413 });
}
chunks.push(value);
}
const bytes = new Uint8Array(total);
let offset = 0;
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
await env.DOCUMENTS.put(key, bytes, { httpMetadata: { contentType: "text/plain" } });
return new Response("Stored", { status: 201 });
}
if (request.method !== "GET") return new Response("Method not allowed", { status: 405, headers: { Allow: "GET, PUT" } });
const object = await env.DOCUMENTS.get(key);
if (object === null) return new Response("Not found", { status: 404 });
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set("ETag", object.httpEtag);
headers.set("Cache-Control", "private, no-store");
return new Response(object.body, { headers });
}
};
JS
get() はキーが存在しない場合に null を返します。ボディを読み取る前に、その状態を処理してください。writeHttpMetadata は保存されているコンテンツタイプを復元し、httpEtag はすでに正しい引用符付き形式になっています。private, no-store により、これらの保護されたドキュメントが共有キャッシュへ保存されるのを防ぎます。
Wrangler がローカル開発用に読み込む .dev.vars に、ランダムなアプリケーショントークンを作成します。これは合成された実験用の認証情報であり、Cloudflare アカウントの認証情報とは別のものです。
umask 077
printf "ACCESS_TOKEN=%s\n" "$(openssl rand -hex 24)" > .dev.vars
デプロイせずに、Wrangler がコードをバンドルできることを確認します。プラットフォームチェックでは、別の一時的なローカルランタイムを新しい合成データで起動し、正確なバイト列、2 通りのサイズ制限処理、大きすぎるオブジェクトが存在しないことを検証します。
npx wrangler deploy --dry-run
ローカルストレージの境界を確認する
このステップでは、ローカル R2 ストレージに対して Worker を実行します。デフォルトでは wrangler dev がローカルシミュレーションを使用するため、これらのリクエストによってクラウド上にオブジェクトが作成されることはありません。開発サーバーをバックグラウンドで起動します。$! には、このジョブのプロセス ID が記録されます。
npx wrangler dev --ip 127.0.0.1 --port 8787 > dev.log 2>&1 &
DEV_PID=$!
dev.log にサーバーの準備完了が表示されるまで待ち、その後、このターミナルに使い捨てのアプリケーショントークンを読み込みます。トークンを表示しないでください。
cat dev.log
set -a
source .dev.vars
set +a
準備済みファイルをアップロードしてダウンロードします。--data-binary はバイト列を保持し、-o はダウンロードを保存します。
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt http://127.0.0.1:8787/documents/report.txt
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/report.txt -o local-download.txt
cmp document.txt local-download.txt
アップロード結果が 201 Stored になり、比較コマンドが何も表示せずに成功することを確認します。存在しないキーと、上限を 1 バイト超えるアップロードをテストします。Python はサイズを制限した合成フィクスチャだけを作成します。
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" http://127.0.0.1:8787/documents/missing.txt
python3 -c "open('oversized.txt','wb').write(b'x' * (1024 * 1024 + 1))"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt http://127.0.0.1:8787/documents/large.txt
404 Not found と 413 Too large になることを確認します。これらの curl 呼び出しでは、期待される HTTP エラーを読み取れるように、意図的に --fail を省略しています。プロキシが返すエラー HTML ページは、アプリケーションのレスポンスではありません。ローカルサーバーを停止する前に、プラットフォームチェックを実行してください。
デプロイしてプライベートバケットとの統合を確認する
このステップでは、実際の R2 上でドキュメント処理を繰り返します。ローカルで成功しても、リモートバインディングやアカウントの所有権が正しいことの証明にはなりません。
開発サーバーを停止して Worker を公開します。
kill "$DEV_PID"
wait "$DEV_PID" 2>/dev/null || true
npx wrangler deploy
標準の一括コマンドでアプリケーションシークレットをアップロードします。.dev.vars は deploy によって自動的にはアップロードされません。
npx wrangler secret bulk .dev.vars
デプロイ出力に表示された正確な HTTPS workers.dev URL を、末尾のスラッシュなしで BASE_URL に設定します。/health が ok を返すまで待ちます。デプロイの反映中であれば、最大 1 分間、読み取りを繰り返してください。
BASE_URL=https://YOUR_WORKER.YOUR_SUBDOMAIN.workers.dev
curl -i "$BASE_URL/health"
レポートをリモートバケットへアップロードし、ダウンロードして比較します。
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @document.txt "$BASE_URL/documents/report.txt"
curl -fsS -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/report.txt" -o remote-download.txt
cmp document.txt remote-download.txt
201 Stored になり、バイト列が一致することを確認します。パブリックエンドポイントに対して、負のケースも繰り返し確認します。
curl -i "$BASE_URL/documents/report.txt"
curl -i -H "Authorization: Bearer $ACCESS_TOKEN" "$BASE_URL/documents/missing.txt"
curl -i -X PUT -H "Authorization: Bearer $ACCESS_TOKEN" --data-binary @oversized.txt "$BASE_URL/documents/large.txt"
401 Unauthorized、404 Not found、413 Too large になることを確認します。Dashboard でこの Worker を開き、R2 バインディングを確認します。次に正確なバケットを開き、documents/report.txt を探します。パブリック開発 URL とカスタムドメインは無効のままです。Worker がアクセス経路を提供していても、すべての Worker ルートが自動的に安全になるわけではありません。バケットがプライベートであることだけで十分とは限りません。

DOCUMENTS 行は、この Worker と対象バケットの接続を示します。例の生成されたリソース名は自分のものとは異なります。

オブジェクト行には report.txt、text/plain、41 B が表示され、Public Access は Disabled のままです。名前と日時は例です。Bucket Size は更新が遅れて 0 B と表示される場合があります。オブジェクト行とダウンロードの成功でレポートの存在を確認できます。
リモートのアプリケーションとバケットを削除する
このステップでは、認証が有効な状態で、この実験の Worker とオブジェクトだけを削除します。Worker を削除しても、プライベートバケットは自動的には消えません。
npx wrangler delete
生成された正確な Worker 名を確認します。アップロードしたオブジェクトを明示的に削除し、その後でバケットを削除します。
BUCKET=$(node -p "JSON.parse(require('fs').readFileSync('wrangler.jsonc')).r2_buckets[0].bucket_name")
npx wrangler r2 object delete "$BUCKET/documents/report.txt" --remote --env-file=.env.management
npx wrangler r2 bucket delete "$BUCKET" --env-file=.env.management
サイズ超過のリクエストによって documents/large.txt が作成されていないことを確認します。バケットが予期せず空でない場合は、このバケットだけを調査し、サイズ契約に失敗した原因を確認してから、正確な合成キーを削除します。この修復が必要になった場合、前の機能チェックには合格していません。
Dashboard で Worker とバケットの一覧を更新し、プラットフォームのクリーンアップチェックを実行します。認証またはネットワークの失敗は、削除成功を意味しません。判定不能として扱ってください。
残った認証情報を閉じる
このステップでは、プロフィールの API Tokens ページでこの実験の管理トークンを失効させ、ローカルのアプリケーションシークレットを削除し、VM の認証を解除します。前のクリーンアップチェックに合格した後でのみ実行してください。
rm .env.management .dev.vars
unset ACCESS_TOKEN
npx wrangler logout
npx wrangler whoami --json || true
loggedIn: false になることを確認します。管理トークンの失効は、Dashboard で別途手動で行うチェックポイントです。ローカルファイルを削除するだけでは、トークンは失効しません。通常の Dashboard ログインや、他の実験で使用するトークンには変更を加えないでください。
まとめ
プライベートな R2 ストレージを Worker にバインドし、サイズ制限付きのアップロードを受け付け、ドキュメントの正確なバイト列をストリーミングし、エラーを処理して、所有するクラウドリソースを削除しました。



