Webアプリケーションで画像や動画などのファイルを扱う際、候補として急速に存在感を増しているのがCloudflare R2です。
本記事では、Cloudflare R2の概要から、よく比較されるD1やAmazon S3との違い、料金体系、そして軽量Webフレームワーク「Hono」を使った具体的な開発例までを分かりやすく解説します。
1. Cloudflare R2とは?
Cloudflare R2は、Cloudflareが提供するS3互換のオブジェクトストレージです。
簡単にいうとGoogle Driveや MicrosoftOneDriveのようなオンラインストレージです。
ただ、WEBアプリなどのプログラムからのみアクセスできるというのが特徴です。
画像、動画、PDF、システムバックアップといった非構造化データ(ファイル)を保存するために使用します。最大の特徴は、一般的なクラウドサービスで発生するデータ転送手数料(エグレス料金)が完全無料($0)である点です。
2. Cloudflare D1 との違い
同じCloudflareのストレージ機能である「D1」との違いに迷う方も多いかもしれません。役割は明確に分かれています。
| 項目 | Cloudflare R2 | Cloudflare D1 |
| データの種類 | 非構造化データ(ファイル本体) | 構造化データ(テーブル・行) |
| 仕組み | S3互換 オブジェクトストレージ | SQLiteベース 分散リレーショナルDB |
| 操作方法 | パス(Key)によるファイルの出力・取得 | SQLクエリ(SELECT, INSERT等) |
| 主な用途 | 画像、動画、PDF、ログファイルの保管 | ユーザー情報、投稿文、設定データの管理 |
基本の組み合わせ:
実務では「画像本体はR2に保存し、そのURLやファイル名・投稿者IDなどのメタデータをD1(SQL)に記録する」という連携パターンが一般的です。
自分はR2だけで全て済むのかと最初思いました。しかしR2はSQLではないため「○月○日以降に作成された画像だけを検索して取り出す」といった複雑な処理(SQLでやるようなこと)ができません。
そのため、「ファイルはR2に保存し、ファイル名や検索用の情報をD1にSQLで記録しておく」という組み合わせでよく使います。
3. 他のクラウドストレージ(Amazon S3など)との比較
オブジェクトストレージの代表格である「Amazon S3」や「Supabase Storage」と比較してみます。
| 項目 | Cloudflare R2 | Amazon S3 | Supabase Storage |
| データ転送量 (Egress) | 完全無料 ($0) | 有料(従量課金) | 無料枠超過後は有料 |
| 無料ストレージ容量 | 10 GB / 月 | 5 GB / 月 (12ヶ月限定) | 1 GB / 月 |
| S3互換性 | あり | 本家 | なし(独自API/SDK) |
| エッジ連携 | Cloudflare Workersと超高速連携 | AWS Lambda等と連携 | Supabaseエコシステム |
Webサイトで頻繁に画像や動画を配信する場合、S3では転送量に応じた課金が膨らむ可能性がありますが、R2であれば通信コストを極限まで抑えることができます。
4. R2の料金体系と無料枠
ここが一番自分が利用している理由になります!
なんといっても無料枠がとても大きいです!
R2の課金要素は「保存量」と「操作回数」の2つのみで、転送通信料はかかりません。
無料枠(毎月更新)
個人開発や小規模なアプリであれば、無料枠内で収まるケースがほとんどです。
- ストレージ容量: 10 GB / 月
- Class A 操作 (書き込み/削除/リスト取得): 100万回 / 月
- Class B 操作 (読み込み/ダウンロード): 1,000万回 / 月
- データ転送量 (Egress): 無制限・無料
無料枠を超えた場合の従量課金
- 保管料: $0.015 / GB / 月
- Class A 操作: $4.50 / 100万回
- Class B 操作: $0.36 / 100万回
これだけの無料枠があれば、個人利用や中小企業の業務アプリならほぼ無料で収まると思います。
5. Cloudflare Workers + Hono + D1 + R2 の構成でプロジェクトをセットアップする手順
ここからは、自分がいつも実務でよく使う軽量フレームワーク Hono と D1+R2 を組み合わせたプロジェクトの立ち上げ手順を解説します。
1つの npm create コマンドだけで全リソースを自動生成することはできないため、① Honoプロジェクトの生成 ➔ ② D1 / R2 バケットの作成 ➔ ③ 接続設定(wrangler.jsonc / wrangler.toml) の順でコマンドを実行します。
1. Honoプロジェクトの生成
npm create hono@latest my-d1+r2-app
✔ Which template do you want to use? cloudflare-workers
✔ Do you want to install project dependencies? Yes
✔ Which package manager do you want to use? npm
cd my-d1+r2-app2. Cloudflare リソース(D1 / R2)の作成
CLIツールの wrangler を使って D1 データベースと R2 バケットを作成します。
① D1 データベースの作成
CLIツールの wrangler を使って D1 データベースを作成します。
npx wrangler d1 create my-db-d1
√ Would you like Wrangler to add it on your behalf? ... yes
√ What binding name would you like to use? ... my_db_d1
√ For local dev, do you want to connect to the remote resource
instead of a local resource? ... no
※ database_id :自動的に wrangler.jsonc に書き込まれるはずです。
② R2 バケットの作成
Wrangler CLIを使って、Cloudflare上にバケットと呼ばれるデータの器を作成します。
npx wrangler r2 bucket create my-bucket
√ Would you like Wrangler to add it on your behalf? ... yes
√ What binding name would you like to use? ... my_bucket_r2
√ For local dev, do you want to connect to the remote resource
instead of a local resource? ... no3. 設定ファイル(wrangler.jsonc または wrangler.toml)の編集
作成した D1 と R2 を Workers から参照できるよう、プロジェクトルートにある設定ファイル(最近のテンプレートでは wrangler.jsonc)にバインディング情報を追記します。
先ほどの npx wrangler コマンドの時、Would you like Wrangler to add it on your behalf? で yes を選択していればここの設定ファイルは自動で書き込まれると思います。
自分はいつも書いていないですが確認だけします。
wrangler.jsonc の場合:
{
"name": "my-app",
"main": "src/index.ts",
"compatibility_date": "2026-08-04",
// D1のバインディング設定
"d1_databases": [
{
"binding": "DB",
"database_name": "my-db",
"database_id": "YOUR_DATABASE_ID_HERE" // 先ほど作成したdatabase_idを貼り付け
}
],
// R2のバインディング設定
"r2_buckets": [
{
"binding": "BUCKET",
"bucket_name": "my-bucket"
}
]
}4. データベースのスキーマ作成
データベースのスキーマ shema.sql を作成します。
CREATE TABLE IF NOT EXISTS files (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
r2_id TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);データベースにスキーマを適用させます。
npx wrangler d1 execute my-db --local --file=./schema.sql5. TypeScript型定義の作成と src/index.ts の記述
TypeScript環境で補完を効かせるため、型定義を指定して Hono アプリを記述します。
今回は簡単なファイル管理のCRUD操作をできるコードを以下に用意しました。
import { Hono } from 'hono'
type Bindings = {
my_db_d1: D1Database
my_bucket_r2: R2Bucket
}
const app = new Hono<{ Bindings: Bindings }>()
// 1. 作成 (CREATE): 画像などのバイナリとタイトルを送信
app.post('/api/files', async (c) => {
const formData = await c.req.formData()
const file = formData.get('file') as File | null
const title = formData.get('title') as string | null
if (!file || !title) {
return c.json({ error: 'ファイルとタイトルは必須です' }, 400)
}
const id = crypto.randomUUID()
const r2Id = `${id}-${file.name}`
// R2にバイナリ保存
await c.env.my_bucket_r2.put(r2Id, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type },
})
// D1にメタデータ保存
await c.env.my_db_d1.prepare(
'INSERT INTO files (id, title, r2_id) VALUES (?, ?, ?)'
).bind(id, title, r2Id).run()
return c.json({ message: '作成完了', id, r2_id: r2Id }, 201)
})
// 2. 読み込み (READ): メタデータ取得
app.get('/api/files/:id', async (c) => {
const id = c.req.param('id')
const record = await c.env.my_db_d1.prepare('SELECT * FROM files WHERE id = ?').bind(id).first()
if (!record) {
return c.json({ error: 'データが見つかりません' }, 404)
}
return c.json(record)
})
// 2-1. バイナリファイルの取得
app.get('/api/files/:id/content', async (c) => {
const id = c.req.param('id')
const record = await c.env.my_db_d1.prepare('SELECT r2_id FROM files WHERE id = ?').bind(id).first<{ r2_id: string }>()
if (!record) {
return c.json({ error: 'データが見つかりません' }, 404)
}
const object = await c.env.my_bucket_r2.get(record.r2_id)
if (!object) {
return c.json({ error: 'R2ファイルが見つかりません' }, 404)
}
const headers = new Headers()
object.writeHttpMetadata(headers)
headers.set('etag', object.httpEtag)
return new Response(object.body, { headers })
})
// 3. 更新 (UPDATE): タイトル変更および/またはファイルの差し替え
app.put('/api/files/:id', async (c) => {
const id = c.req.param('id')
const formData = await c.req.formData()
const newFile = formData.get('file') as File | null
const newTitle = formData.get('title') as string | null
const record = await c.env.my_db_d1.prepare('SELECT * FROM files WHERE id = ?').bind(id).first<{ r2_id: string, title: string }>()
if (!record) {
return c.json({ error: 'データが見つかりません' }, 404)
}
let r2Id = record.r2_id
let title = newTitle || record.title
// 新しいファイルがあれば旧ファイルを消して新ファイルを保存
if (newFile && newFile.size > 0) {
await c.env.my_bucket_r2.delete(record.r2_id)
r2Id = `${id}-${newFile.name}`
await c.env.my_bucket_r2.put(r2Id, await newFile.arrayBuffer(), {
httpMetadata: { contentType: newFile.type },
})
}
// D1のメタデータ更新
await c.env.my_db_d1.prepare(
'UPDATE files SET title = ?, r2_id = ? WHERE id = ?'
).bind(title, r2Id, id).run()
return c.json({ message: '更新完了', id, title, r2_id: r2Id })
})
// 4. 消去 (DELETE): D1とR2の両方から削除
app.delete('/api/files/:id', async (c) => {
const id = c.req.param('id')
const record = await c.env.my_db_d1.prepare('SELECT r2_id FROM files WHERE id = ?').bind(id).first<{ r2_id: string }>()
if (!record) {
return c.json({ error: 'データが見つかりません' }, 404)
}
// R2からバイナリ削除
await c.env.my_bucket_r2.delete(record.r2_id)
// D1からレコード削除
await c.env.my_db_d1.prepare('DELETE FROM files WHERE id = ?').bind(id).run()
return c.json({ message: '削除完了', id })
})
export default app※TypeScriptの型定義不足エラーが出た場合
自分がハマったポイントとして、VScode等のエディターで 名前 ‘File’ が見つかりません。 などのエラーが多数出た場合があります。
これはTypeScriptの型定義(tsconfig.json)に Cloudflare Workers や Web 標準の型が含まれていないことが原因です。
解決手順
1. パッケージのインストール ターミナルで型定義パッケージをインストールします。
npm install -D @cloudflare/workers-types2. tsconfig.json の編集 tsconfig.json の compilerOptions 内にある types に @cloudflare/workers-types を追加します(DOM などの Web 標準型を含める場合は lib も設定します)。
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": [
"ESNext",
"DOM"
],
"types": [
"@cloudflare/workers-types"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
}
}tsconfig.json とは?(このファイルの意味)
tsconfig.json は TypeScriptコンパイラ(tsc)の動作設定ファイル です。
「どのJavaScriptのバージョンに向けて変換するか」「どれくらい厳密に型をチェックするか」「どんなグローバル型(D1Database や File など)を認識させるか」という 開発環境のルールブック の役割を果たします。
※ エディタ(VS Codeなど)で赤波線が消えない場合は、エディタを再起動するか、コマンドパレットから 「TypeScript: TS サーバーを再起動」 を実行してください。
6. 開発サーバーの起動
npm run devローカル開発環境(Wrangler)では、D1 や R2 のローカルエミュレータが自動的に起動するため、クラウドに影響を与えることなくテストが可能です。
- アップロードのテスト
# ファイルのパスは適宜変更してください
curl.exe -X POST http://localhost:8787/api/files `
-F "title=テスト画像" `
-F "file=@C:\Users\user\Pictures\img_1.png"
# 以下のようなメッセージが表示されれば完了
{"message":"作成完了","id":"73eccc43-149c-4fee-9975-1cd7f065a2d9",
"r2_id":"73eccc43-149c-4fee-9975-1cd7f065a2d9-img_1.png"} - メタデータ取得 (READ)
# 取得するファイルのidを入力してください
curl.exe http://localhost:8787/api/files/ここにid- 画像ファイルダウンロード (READ Content)
# ダウンロードするファイルのidを入力してください
curl.exe http://localhost:8787/api/files/ここにid/content --output downloaded_img.png- 更新 (UPDATE)
# タイトルとファイルを同時に更新する場合
curl.exe -X PUT http://localhost:8787/api/files/ここにid `
-F "title=更新後のタイトル" `
-F "file=@C:\Users\user\Pictures\img_2.png"- 消去 (DELETE)
curl.exe -X DELETE http://localhost:8787/api/files/ここにidまとめ
Cloudflare R2は、転送量無料・10GBの太っ腹な無料枠・S3互換という強力な特徴を備えたストレージサービスです。
HonoやWorkersと組み合わせることで、爆速かつ低コストなWeb APIやメディア配信サーバーを数分で構築できます。個人開発から商用サービスまで、ストレージの第一選択肢としてぜひ試してみてください!

コメント