跳至主要內容

Cloudflare R2 物件儲存:零出口費用的 S3 替代方案

Cloudflare R2 物件儲存:零出口費用的 S3 替代方案

AWS S3 是業界標準的物件儲存服務,但它有一個讓人頭痛的費用結構:出口費用(Egress)。當你的資料從 S3 傳輸到外部時,每 GB 都要收費。對於高流量的應用,這個費用可能非常可觀。Cloudflare R2 提供了 S3 相容的 API,而且完全不收出口費用

R2 的費用結構

服務 儲存費用 出口費用
AWS S3 $0.023/GB $0.09/GB
Cloudflare R2 $0.015/GB $0(零!)

前 10 GB 儲存和每月前 1000 萬次 A 類操作免費。

建立 R2 Bucket

透過 Cloudflare Dashboard

  1. 登入 Cloudflare Dashboard
  2. 前往 R2 Object Storage
  3. 點擊 "Create bucket"
  4. 輸入 Bucket 名稱和選擇地區

透過 Wrangler CLI

# 安裝 Wrangler
npm install -g wrangler

# 登入
wrangler login

# 建立 Bucket
wrangler r2 bucket create my-files

# 列出 Bucket
wrangler r2 bucket list

取得 API 憑證

Cloudflare Dashboard → R2 → Manage R2 API Tokens
→ Create API Token
→ 選擇權限(Object Read & Write)
→ 記下 Access Key ID 和 Secret Access Key

使用 AWS SDK 操作 R2

R2 完全相容 S3 API,可以直接用 AWS SDK:

import { S3Client, PutObjectCommand, GetObjectCommand,
         DeleteObjectCommand, ListObjectsV2Command } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const r2 = new S3Client({
  region: 'auto',
  endpoint: `https://${process.env.CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: {
    accessKeyId: process.env.R2_ACCESS_KEY_ID!,
    secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!,
  },
});

const BUCKET = 'my-files';

// 上傳檔案
async function uploadFile(key: string, body: Buffer | Uint8Array, contentType: string) {
  await r2.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    Body: body,
    ContentType: contentType,
  }));
  return key;
}

// 下載檔案
async function downloadFile(key: string): Promise<Buffer> {
  const response = await r2.send(new GetObjectCommand({
    Bucket: BUCKET,
    Key: key,
  }));

  const chunks: Uint8Array[] = [];
  for await (const chunk of response.Body as AsyncIterable<Uint8Array>) {
    chunks.push(chunk);
  }
  return Buffer.concat(chunks);
}

// 刪除檔案
async function deleteFile(key: string) {
  await r2.send(new DeleteObjectCommand({
    Bucket: BUCKET,
    Key: key,
  }));
}

// 列出檔案
async function listFiles(prefix?: string) {
  const response = await r2.send(new ListObjectsV2Command({
    Bucket: BUCKET,
    Prefix: prefix,
    MaxKeys: 100,
  }));
  return response.Contents || [];
}

// 生成預簽名 URL(有時效的下載連結)
async function getPresignedUrl(key: string, expiresIn = 3600): Promise<string> {
  return getSignedUrl(
    r2,
    new GetObjectCommand({ Bucket: BUCKET, Key: key }),
    { expiresIn }
  );
}

在 Next.js API Route 中使用

// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { r2, BUCKET } from '@/lib/r2';
import { PutObjectCommand } from '@aws-sdk/client-s3';
import { nanoid } from 'nanoid';

export async function POST(req: NextRequest) {
  const formData = await req.formData();
  const file = formData.get('file') as File;

  if (!file) {
    return NextResponse.json({ error: '未找到檔案' }, { status: 400 });
  }

  const maxSize = 10 * 1024 * 1024; // 10MB
  if (file.size > maxSize) {
    return NextResponse.json({ error: '檔案大小超過限制' }, { status: 400 });
  }

  const ext = file.name.split('.').pop();
  const key = `uploads/${nanoid()}.${ext}`;
  const buffer = Buffer.from(await file.arrayBuffer());

  await r2.send(new PutObjectCommand({
    Bucket: BUCKET,
    Key: key,
    Body: buffer,
    ContentType: file.type,
    Metadata: {
      originalName: file.name,
    },
  }));

  const url = `${process.env.R2_PUBLIC_URL}/${key}`;
  return NextResponse.json({ url, key });
}

設定自訂網域(公開 Bucket)

讓 R2 檔案可以透過你的網域直接存取:

Cloudflare Dashboard → R2 → 你的 Bucket → Settings
→ Public Access → Connect Domain
→ 輸入子網域(如 files.example.com)

這樣 R2 中的檔案就可以透過 https://files.example.com/path/to/file.jpg 存取,且透過 Cloudflare CDN 分發,速度快,出口費用為零。

在 Cloudflare Workers 中直接使用

Workers 可以直接綁定 R2,無需 HTTP 請求:

// wrangler.toml
// [[r2_buckets]]
// binding = "MY_BUCKET"
// bucket_name = "my-files"

export interface Env {
  MY_BUCKET: R2Bucket;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const url = new URL(request.url);
    const key = url.pathname.slice(1);

    if (request.method === 'GET') {
      const object = await env.MY_BUCKET.get(key);
      if (!object) {
        return new Response('Not Found', { status: 404 });
      }

      return new Response(object.body, {
        headers: {
          'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
          'Cache-Control': 'public, max-age=86400',
          'ETag': object.etag,
        },
      });
    }

    if (request.method === 'PUT') {
      await env.MY_BUCKET.put(key, request.body, {
        httpMetadata: { contentType: request.headers.get('Content-Type') || '' },
      });
      return new Response('OK');
    }

    return new Response('Method Not Allowed', { status: 405 });
  },
};

從 AWS S3 遷移

R2 的 S3 相容性讓遷移非常簡單,只需修改端點:

// 原本的 S3 設定
const s3 = new S3Client({
  region: 'ap-northeast-1',
  credentials: { ... },
});

// 改為 R2,只需修改 endpoint 和 region
const r2 = new S3Client({
  region: 'auto',  // R2 固定使用 'auto'
  endpoint: `https://${CF_ACCOUNT_ID}.r2.cloudflarestorage.com`,
  credentials: { ... },
});

// 其他程式碼完全不用改!

適用場景

R2 的最佳使用場景:

  • 使用者上傳的圖片、影片、文件
  • 靜態資源(配合 Cloudflare CDN)
  • 備份檔案
  • 大量下載的資料集

仍選擇 S3 的情況:

  • 需要與 AWS 生態深度整合(Lambda、CloudFront OAI)
  • 需要特定 S3 進階功能(Object Lock、Intelligent Tiering)

總結

Cloudflare R2 的零出口費用對於高流量應用是巨大的成本優勢。S3 相容 API 讓遷移成本極低,只需修改端點設定。如果你的應用有大量的檔案讀取需求,R2 + Cloudflare CDN 的組合幾乎是目前最划算的物件儲存方案。

分享這篇文章