Cloudflare ハンズオン教科書|2026-08 版
第 6 章

R2 — エグレス無料のオブジェクトストレージ

S3 互換ストレージで画像アップローダーを作る

この章で学ぶこと

R2 とは — エグレス無料の S3 互換ストレージ

R2 は Cloudflare のオブジェクトストレージです。画像・動画・バックアップ・機械学習のモデルファイルといった非構造化データを「オブジェクト」として「バケット」に格納します。前章の KV が小さな値の高速読み取りに特化していたのに対し、R2 は大きなバイナリの保存と配信が主戦場です。

最大の特徴はエグレス(インターネットへのデータ転送)が無料であることです。従来のクラウドストレージはデータの取り出しに従量課金があり、配信量が伸びるほど転送料が膨らみます。R2 ではこれがゼロで、課金対象はストレージ容量と操作回数が中心です(低頻度アクセス向けの Infrequent Access ストレージクラスのみ取り出し料金が加わります。詳細は後述の料金モデル)。

書き込み — アップロードした分だけ保存量が増える ブラウザ Worker R2 バケット PUT /cat.png put()=操作課金 保存量に課金 読み取り — 何 GB 配信してもエグレスは無料 R2 バケット Worker 世界の利用者 get()=操作課金 エグレス=無料
図 6-1 課金されるのは「どれだけ保存しているか」と「何回操作したか」の 2 つだけです。R2 から出ていく転送量そのものは無料なので、配信する GB 数が 10 倍になっても増えるのは操作回数分だけです。

もう 1 つの柱が S3 互換 API です。Amazon S3 の API と互換性があるため、aws-sdk・boto3・rclone といった既存の S3 ツールが、エンドポイントと認証情報の差し替えだけでそのまま動きます。既存アプリからの移行や、S3 エコシステムの資産を活かした運用がしやすい設計です。

整合性のモデルも押さえておきましょう。R2 の書き込みと削除は強い整合性を持ちます。put() の Promise が解決した時点で、以降の読み取りは世界中どこからでも新しい値を返します。KV の結果整合性(伝播に時間がかかる)との大きな違いです。

R2 には 3 つの API サーフェスがあります。

APIアクセス方法主な用途
Workers APIバケットを Worker にバインドし env 経由で操作Worker 内での読み書き。この章の主役
S3 互換 APIhttps://<ACCOUNT_ID>.r2.cloudflarestorage.com既存の S3 SDK・ツール(aws-sdk、rclone 等)からの利用、プリサインド URL
Cloudflare REST APIapi.cloudflare.comダッシュボードや wrangler が使う管理系 API。5 分あたり 1,200 リクエストのレート制限あり

主要な制限(2026-08 時点)は次のとおりです。

項目上限
バケットあたりの容量・オブジェクト数無制限
オブジェクトサイズ5 TiB(正確には 4.995 TiB)
1 回のアップロード(単一 PUT / multipart の 1 パート)5 GiB(正確には 4.995 GiB)
multipart のパート数10,000
オブジェクトキー長1,024 バイト
オブジェクトメタデータ8,192 バイト
同一キーへの同時書き込み毎秒 1 回(超過は 429 応答)

バケット作成と wrangler r2 コマンド

バケットはダッシュボードでも wrangler でも作成できます。バケット名には次の制約があります。

wrangler の R2 サブコマンドでバケットとオブジェクトの基本操作が一通りできます。

コマンド説明
r2 bucket create [NAME]バケット作成。--location で配置ヒント、--storage-class で既定ストレージクラスを指定可能
r2 bucket listアカウント内のバケット一覧
r2 bucket info [BUCKET]バケットの情報を表示
r2 bucket delete [BUCKET]バケット削除(先に中身を空にする必要がある)
r2 object put [BUCKET/KEY]オブジェクト作成。--file でファイル指定、--content-type 等でメタデータ指定
r2 object get [BUCKET/KEY]オブジェクト取得。--file で保存先指定、--pipe で標準出力へ
r2 object delete [BUCKET/KEY]オブジェクト削除
r2 bucket dev-url enable [BUCKET]r2.dev 公開 URL の有効化
r2 bucket domain add [BUCKET]カスタムドメインの接続(--domain と --zone-id が必須)
r2 bucket cors set [BUCKET]CORS ポリシーの設定

オブジェクトパスは {バケット名}/{キー} の形式です。r2 object 系コマンドには --local / --remote フラグがあり、ローカル開発用ストレージと本番バケットのどちらを操作するかを明示できます。本物のバケットを操作したいときは --remote を付けると確実です。

wrangler の R2 サポートは基本操作向けです。大量ファイルの一括アップロードや他ストレージからのミラーリングには、rclone などの S3 互換ツールが公式に推奨されています。

Worker バインディングで put / get / delete / list

バインディングの定義

Worker から R2 を使うには、KV と同じくバインディングを wrangler.jsonc に定義します。binding が Worker コード内での変数名、bucket_name が実際のバケット名です。

{
  "r2_buckets": [
    {
      "binding": "MY_BUCKET",
      "bucket_name": "<YOUR_BUCKET_NAME>"
    }
  ]
}

<YOUR_BUCKET_NAME> は自分のバケット名(このあとのハンズオンでは textbook-uploads)に置き換えます。これで env.MY_BUCKET からバケットを操作できます。

基本メソッド

メソッド動作
put(key, value, options?)オブジェクトを保存し、メタデータの R2Object を返す。onlyIf の事前条件が満たされないと null を返し保存しない
get(key, options?)本文つきの R2ObjectBody を返す。キーが存在しなければ null
head(key)本文なしのメタデータのみ取得。存在しなければ null
delete(key)削除。キーの配列を渡せば 1 回で最大 1,000 件まで削除できる
list(options?)キーの辞書順で最大 1,000 件を列挙。prefix / limit / cursor / delimiter を指定可能
createMultipartUpload(key)multipart アップロードを開始し R2MultipartUpload を返す
resumeMultipartUpload(key, uploadId)進行中の multipart アップロードの操作オブジェクトを得る

実装時に効いてくる仕様をまとめます。

multipart アップロード

1 回のアップロード上限(約 5 GiB)を超えるファイルや、失敗時の再開・並列転送が必要なケースでは multipart アップロードを使います。createMultipartUpload() で開始し、uploadPart(partNumber, value) でパートを送り、各パートの etag と番号を集めて complete(parts) で確定する流れです。パートは最後を除き 5 MiB 以上かつ同一サイズで、最大 10,000 パート・合計約 5 TiB まで扱えます。未完了の multipart アップロードは 7 日後に自動で中断されます。単一 PUT との使い分けの目安は「100 MB を超えるあたりから multipart」です。なお S3 互換 SDK や rclone は閾値を超えると自動的に multipart へ切り替えるため、S3 API 経由なら自前実装は通常不要です。

失敗したパートだけ再送 開始 uploadId 取得 パート送信 5 MiB 以上ずつ 完了 complete(parts) uploadPart() を繰り返す etag を集めて確定 7 日放置すると 未完了分は 自動で中断
図 6-2 パートを送り終えても、complete() が成功して初めて 1 つのオブジェクトになります。途中のパートは個別にやり直せる一方、完了しないまま放置されたアップロードは 7 日後に自動で中断されます。

プリサインド URL

プリサインド URL は、S3 API の署名をクエリパラメータとして URL に埋め込み、API キーを渡さずに特定オブジェクトへの一時アクセスを許可する仕組みです。ブラウザから R2 へ直接アップロードさせたい(Worker のリクエストサイズ上限を回避したい)場合や、期限付きダウンロードリンクの配布に向きます。有効期限は 1 秒〜7 日(604,800 秒)で、GET / HEAD / PUT / DELETE に対応します(HTML フォームの POST は非対応)。生成には R2 API トークン(Access Key ID / Secret Access Key)が必要で、R2 との通信なしにクライアント側だけで生成できます。

A. Worker 経由でアップロード ブラウザ ① 画像の本文 Worker ② put() R2 バケット 本文が Worker を通る B. プリサインド URL で直接アップロード ブラウザ Worker R2 とは通信しない ① URL 要求 ② 署名 URL ③ 本文を直接 PUT R2 バケット 本文は Worker を通らない
図 6-3 色付きの矢印の行き先が違いのすべてです。A は本文が Worker を通るため Worker のリクエストサイズ上限を受けます。B は署名 URL を取りに行く往復が 1 つ増える代わりに、本文はブラウザから R2 へ直行し、Worker は URL を作るだけ(R2 との通信すら発生しない)で済みます。
import { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const S3 = new S3Client({
  region: "auto", // SDK 上必須だが R2 では使われない
  endpoint: `https://<ACCOUNT_ID>.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: "<ACCESS_KEY_ID>",
    secretAccessKey: "<SECRET_ACCESS_KEY>",
  },
});

// 1 時間有効なアップロード用 URL(Content-Type も署名に含めて制限)
const putUrl = await getSignedUrl(
  S3,
  new PutObjectCommand({
    Bucket: "textbook-uploads",
    Key: "image.png",
    ContentType: "image/png",
  }),
  { expiresIn: 3600 },
);

URL を持つ人は誰でも操作できるベアラートークン相当のものなので、必要最小限の有効期限で発行し、共有先を限定してください。

公開方法と料金モデル

パブリックバケット

バケットはデフォルトで非公開です。コンテンツを直接インターネットへ公開するには 2 つの方法があります。

# 開発用 r2.dev URL を有効化
npx wrangler r2 bucket dev-url enable textbook-uploads

# カスタムドメインを接続(ゾーンは同一アカウントにあること)
npx wrangler r2 bucket domain add textbook-uploads \
  --domain files.example.com --zone-id <ZONE_ID>

<ZONE_ID> はダッシュボードの対象ゾーン概要ページで確認できるゾーン ID に置き換えます。なお、公開バケットではドメイン直下でのオブジェクト一覧表示はできません。また WAF や Access で保護する場合は、抜け道にならないよう r2.dev 側の公開を無効化しておく必要があります。

同じバケットに、性質の違う 2 つの入口ができる 訪問者 カスタム ドメイン キャッシュ WAF・Access R2 バケット HTTPS 必ず通過 ミス時 r2.dev を有効にしたままだと残る抜け道
図 6-4 カスタムドメインを接続すると、キャッシュや WAF がバケットの前段に入ります。ただし r2.dev を有効にしたままだと、同じオブジェクトへ前段を素通りして届く経路が残ります。この経路ではキャッシュも WAF も効かず、毎秒数百リクエストを超えると 429 が返ります。前段で守りたいなら r2.dev 側は無効化します。

料金モデル

R2 の課金は「ストレージ容量」と「操作回数」の 2 軸です。操作は状態を変更する Class A(PutObject、ListObjects、CopyObject、multipart 系など)と、読み取り中心の Class B(GetObject、HeadObject など)に分かれ、Class A の方が高単価です。DeleteObject・DeleteBucket・AbortMultipartUpload は無料です(2026-08 時点)。

項目StandardInfrequent Access無料枠(Standard のみ)
ストレージ$0.015 / GB-月$0.01 / GB-月10 GB-月 / 月
Class A 操作$4.50 / 100 万件$9.00 / 100 万件100 万件 / 月
Class B 操作$0.36 / 100 万件$0.90 / 100 万件1,000 万件 / 月
データ取り出しなし$0.01 / GB—
エグレス無料無料無料

ストレージは GB-月(1 日ごとのピーク使用量を 30 日で平均した値)で計測され、使用量は次の課金単位に切り上げられます。低頻度アクセス向けの Infrequent Access ストレージクラスは保存単価が下がる代わりに操作単価と取り出し料金が上乗せされ、30 日の最低保存期間があります。認可されなかったリクエスト(401)には操作課金が発生しません。

ハンズオン 6-1|バケットを作って wrangler で読み書きする

この章で使うバケットを作成し、wrangler だけでオブジェクトの出し入れを体験します。所要時間は 10 分程度です。

事前準備: R2 を初めて使う場合、ダッシュボードの Storage & databases > R2 > Overview でチェックアウトフローを完了し、R2 サブスクリプションを有効化する必要があります。無料枠(前節参照)の範囲内であれば料金はかかりません。

  1. バケットを作成し、一覧で確認する。
    npx wrangler r2 bucket create textbook-uploads
    npx wrangler r2 bucket list

    list の出力に textbook-uploads が含まれていれば成功です。

  2. テキストファイルをアップロードし、取得してみる。--remote で本番バケットを明示的に指定する。
    echo "hello r2" > hello.txt
    npx wrangler r2 object put textbook-uploads/hello.txt --file hello.txt --remote
    npx wrangler r2 object get textbook-uploads/hello.txt --pipe --remote

    最後のコマンドが hello r2 を出力すれば、書き込みと読み取りの両方が確認できています。

  3. オブジェクトを削除する。
    npx wrangler r2 object delete textbook-uploads/hello.txt --remote

    もう一度 get するとオブジェクトが存在しない旨のエラーになります。バケット自体は次のハンズオンで使うため残しておきます。

ハンズオン 6-2|画像アップローダー Worker を作る

PUT でアップロード、GET で配信、DELETE で削除、ルートパスで一覧という 4 機能を持つファイルアップローダー Worker を作ります。所要時間は 20 分程度です。

  1. 新しいプロジェクトを作成する。質問には「Hello World example」「Worker only」「TypeScript」、デプロイは「No」を選ぶ。
    npm create cloudflare@latest -- r2-uploader
    cd r2-uploader
  2. wrangler.jsonc に R2 バインディングを追記する。
    {
      // ...既存の設定はそのまま残す
      "r2_buckets": [
        {
          "binding": "MY_BUCKET",
          "bucket_name": "textbook-uploads"
        }
      ]
    }
  3. src/index.ts を次の内容に置き換える。
    interface Env {
      MY_BUCKET: R2Bucket;
    }
    
    export default {
      async fetch(request, env): Promise<Response> {
        const url = new URL(request.url);
        const key = url.pathname.slice(1);
    
        switch (request.method) {
          case "PUT": {
            const object = await env.MY_BUCKET.put(key, request.body, {
              httpMetadata: request.headers, // Content-Type などを保存
            });
            return Response.json({ key: object.key, size: object.size });
          }
          case "GET": {
            if (key === "") {
              // ルートパスは一覧を返す
              const listed = await env.MY_BUCKET.list();
              return Response.json(
                listed.objects.map((o) => ({ key: o.key, size: o.size })),
              );
            }
            const object = await env.MY_BUCKET.get(key);
            if (object === null) {
              return new Response("Object Not Found", { status: 404 });
            }
            const headers = new Headers();
            object.writeHttpMetadata(headers); // 保存時のメタデータを復元
            headers.set("etag", object.httpEtag);
            return new Response(object.body, { headers });
          }
          case "DELETE": {
            await env.MY_BUCKET.delete(key);
            return new Response("Deleted!");
          }
          default:
            return new Response("Method Not Allowed", {
              status: 405,
              headers: { Allow: "PUT, GET, DELETE" },
            });
        }
      },
    } satisfies ExportedHandler<Env>;
  4. npx wrangler dev で起動し、別のターミナルから動作確認する。アップロード時は必ず --data-binary を使う。
    curl -X PUT http://localhost:8787/hello.txt --data-binary "hello r2"
    #=> {"key":"hello.txt","size":8}
    
    curl http://localhost:8787/hello.txt
    #=> hello r2
    
    # 手元の画像を Content-Type つきでアップロード
    curl -X PUT http://localhost:8787/cat.png \
      -H "Content-Type: image/png" --data-binary @cat.png
    
    curl http://localhost:8787/
    #=> [{"key":"cat.png","size":...},{"key":"hello.txt","size":8}]

ブラウザで http://localhost:8787/cat.png を開くと、保存した Content-Type が writeHttpMetadata で復元されるため画像として表示されます。なお wrangler dev は既定でローカルストレージに読み書きするため、ここでのオブジェクトは本番バケットには入っていません(つまずきポイント参照)。

ハンズオン 6-3|認証を付けてデプロイする

このままデプロイすると誰でもアップロード・削除ができてしまいます。書き込み系操作を事前共有キーで保護してから本番へデプロイします。所要時間は 15 分程度です。

  1. src/index.ts の先頭に認可ロジックを追加し、Env にシークレットの型を足す。
    interface Env {
      MY_BUCKET: R2Bucket;
      AUTH_KEY_SECRET: string;
    }
    
    const hasValidHeader = (request: Request, env: Env) =>
      request.headers.get("X-Custom-Auth-Key") === env.AUTH_KEY_SECRET;
    
    function authorizeRequest(request: Request, env: Env): boolean {
      switch (request.method) {
        case "PUT":
        case "DELETE":
          return hasValidHeader(request, env); // 書き込みは秘密ヘッダー必須
        default:
          return true; // GET(配信・一覧)は公開のまま
      }
    }
    
    // fetch() の先頭に次のガードを追加する
    // if (!authorizeRequest(request, env)) {
    //   return new Response("Forbidden", { status: 403 });
    // }
  2. シークレットを登録し、デプロイする。ローカル開発時のシークレットには第 3 章で扱った .dev.vars を使う。
    npx wrangler secret put AUTH_KEY_SECRET
    # プロンプトに秘密の文字列を入力
    #=> Success! Uploaded secret AUTH_KEY_SECRET.
    
    npx wrangler deploy
  3. デプロイ先 URL で認可の動作を確認する。
    # ヘッダーなし → 拒否される
    curl -X PUT https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png \
      --data-binary @cat.png
    #=> Forbidden
    
    # 正しいヘッダーつき → 成功
    curl -X PUT https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png \
      -H "X-Custom-Auth-Key: <YOUR_SECRET>" --data-binary @cat.png
    #=> {"key":"cat.png","size":...}
    
    # 配信は認証なしで通る
    curl -o downloaded.png https://r2-uploader.<YOUR_SUBDOMAIN>.workers.dev/cat.png

    <YOUR_SUBDOMAIN> は自分の workers.dev サブドメイン、<YOUR_SECRET> は手順 2 で登録した文字列に置き換えます。今回はデプロイ後なので、オブジェクトは本番の textbook-uploads バケットに書き込まれ、ダッシュボードからも確認できます。

つまずきポイント

wrangler dev でアップロードしたファイルが本番バケットにない — wrangler dev は既定でローカル開発モードで動き、R2 操作はマシン上のローカルストレージに対して実行されます。ダッシュボードに出ないのは仕様です。開発中に本物のバケットへつなぎたい場合は、バインディング設定に "remote": true を付けます。wrangler r2 object コマンドも --remote / --local で対象を明示できます。

curl でアップロードしたファイルが壊れる — curl の -d(--data)はテキスト送信用で、バイナリが途中で切り詰められます。R2 へのファイル送信では必ず --data-binary を使ってください。

r2.dev URL で 429 Too Many Requests が返る — r2.dev サブドメインは開発用で、毎秒数百リクエスト規模のレート制限とスループット制限がかかります。本番トラフィックはカスタムドメインを接続して配信してください。カスタムドメインならキャッシュや WAF も併用できます。

プリサインド URL がカスタムドメインで使えない — プリサインド URL は S3 API のエンドポイント(<ACCOUNT_ID>.r2.cloudflarestorage.com)専用です。カスタムドメイン経由のアクセスに認証を付けたい場合は、WAF の HMAC 検証機能(Pro プラン以上)を使います。

理解度チェック

Q1. R2 のエグレス(インターネットへのデータ転送)にはいくら課金される?

無料です。どのストレージクラスでもエグレス課金はありません。課金対象はストレージ容量(GB-月)と Class A / Class B 操作の回数(Infrequent Access では加えてデータ取り出し料金)です。

Q2. Class A 操作と Class B 操作の違いは?

Class A は PutObject や ListObjects など状態を変更・列挙する操作で単価が高く、Class B は GetObject や HeadObject など読み取り中心の操作で単価が安く設定されています。なお DeleteObject などの削除系は無料です。

Q3. 1 回の PUT でアップロードできる上限と、それを超えるファイルの扱いは?

単一アップロードの上限は約 5 GiB(正確には 4.995 GiB)です。それを超えるファイルは multipart アップロードで分割して送り、最大 10,000 パート・合計約 5 TiB まで扱えます。

Q4. プリサインド URL の最長有効期限と、使えるエンドポイントは?

最長 7 日間(604,800 秒)です。S3 API エンドポイント(<ACCOUNT_ID>.r2.cloudflarestorage.com)でのみ有効で、カスタムドメインでは使えません。

Q5. list() の結果に続きがあるかどうかは何で判定する?

truncated プロパティで判定します。true なら cursor を次の list() 呼び出しに渡して続きを取得します。返却件数と limit の比較で判定すると、メタデータ量の調整で件数が減った場合に誤動作します。

まとめと次章へ

ファイルのような非構造化データの置き場所は手に入りました。次章では構造化データの番です。SQLite ベースのサーバーレス SQL データベース D1 で、リレーショナルな API を組み立てます。