天然パーマです。

Cloudflare R2もいいぞ!

CloudflareがSQLデータベースD1をアナウンスして衝撃を受けましたが、「R2もいいぞ!」というお話。 R2はS3みたいなストレージエンジンで、Sの前「R」、3の前の「2」ともじってて、AWS対抗といえます。で、ちょうどD1がアナウンスされた昨日にベータオープンしました。

「ただのストレージだろ」とたかをくくってたんですが、使ってみてだいぶよいです。 先にまとめると以下の3つのことが言えます。

  1. 安い
  2. DX(Developer Experience)がよい
  3. Cloudflareの他の製品を組み合わせるとヤバい

詳しく紹介します。

作ったアプリ

R2を評価するために、以前S3をバックエンドにして作ったアプリケーションをR2に置き換えるというのをやりました。そのアプリケーションについては記事を書いてます。

Cloudflare Workersで作っていて、機能は以下の通り。

  1. 画像をBase64エンコードした文字列を受け取る
  2. 文字列からMIMEタイプを判別
  3. デコードしてバイナリをS3に保存
  4. ファイルがアップロードされた先のURLをレスポンスで返す

これ、macOSの「ショートカット」と組み合わせるとGyazoみたいになってすごい便利です。

今回は「S3の部分をR2に変換」してみました。最終的にはこのような構成になりました。

  • ファイル置き場としてのR2
  • R2をバックエンドにCloudflare Workersが画像を配信する
  • Cloudflare KVでキャッシュをする

成果物のリポジトリはこちらです。

では今回の経験を元に、先ほどの3つについて見ていきましょう。

1. 安い

まず公式が一番に謳ってるのがS3と比べて「安い」ということです。 なにがすごいってエグレス(下り料金)無料です。 その他、無料枠がどれもS3の倍以上あってすごい。

  • ストレージ => 10 GB(S3は5G)
  • 更新系の操作 => 1,000,000回 / month(S3は2,000件)
  • 取得系の操作 => 10,000,000回 / month(S3は20,000件)

Cloudflare WorkersやKVも無料で使えるので、個人レベルだったら十分枠の中で遊べます。

追記

料金については誤解を招きやすいので、公式のドキュメントから引用。 ストレージ「10GB」とあるのは厳密には「10GB/月」となりまして、「GB/月」ってなんじゃ、というと以下の通りです。

Storage is billed using gigabyte-month (GB-month) as the billing metric. A GB-month is calculated by recording total bytes stored for the duration of the month.

For example:

Storing 1 GB for 30 days will be charged as 1 GB-month.

Storing 2 GB for 15 days will be charged as 1 GB-month.

2. DX(Developer Experience)が良い

R2はS3互換のAPIを持っていてそれによって移行が簡単にできるという謳い文句がありますが、加えてCloudflare Workersから操れます。バケットに置いたファイルは基本的にはプライベートで、パブリックにするにはWorkersを使わなくてはいけないらしく、Cloudflare Workers前提ですね。実際にアプリを作ってみると、このCloudflare Workersを使った開発体験がとてもいい。まず、JavaScriptのランタイムAPIが提供されています。 いわゆる「SDK」とは呼ばず、「ランタイムAPI」と呼んでいるのがポイントです。

つまりバケットからオブジェクトを取得するには簡潔にこうです。

const object = await BUCKET.get(key);
const data = await object.arrayBuffer();
return new Response(data, {
  status: 200,
  headers: {
    "Content-Type": object.httpMetadata.contentType,
  },
});

objectではarrayBufferを取れるだけじゃなく、object.text()object.json()などいった、Fetch APIのRequestっぽいAPIを備えています。Cloudflare Workersをいじってる者としては馴染みがあります。

また、先日「2.0」がリリースされたWranglerがR2に完全対応していて、その使い勝手がよいです。

WranglerはCloudflare Workersの開発からデプロイ、さらにはログの閲覧までをサポートするCLIです。R2のベータ公開に合わせてR2用のコマンドが用意されました。

R2バケットの作成、リスト、オブジェクトの閲覧ができます。 それだけなら、まぁあれですが、「R2が」「Cloudflare Workersの」CLIに組み込まれているということから、やはりR2はWorkersから使われる前提であることが分かります。

さて、実用的なアプリケーションを作ってみましょう。

Wranglerコマンドで「image」という名前のR2のバケットを作ります。

wrangler r2 bucket create images

次に、wrangler.tomlに設定を書いてバケットをバインディングさせます。 preview_bucket_nameの指定もできるので、開発環境と本番で分けることもできます。

[[r2_buckets]]
binding = 'BUCKET'
bucket_name = 'images'
preview_bucket_name = 'images'

Cloudflare WorkersからはこのBUCKETという名前でR2にアクセスできるようになります。 今回はこういうコードを書きました。 PUT /upload でbase64の文字列が入ったJSONを受け取り、その文字列をもとにMIMEタイプを取得。 ハッシュキーも作成しつつ、デコードしてバイナリ化。 それを先ほどのMIMEタイプとともにバケットにputしています。

app.put("/upload", async (c) => {
  const data = await c.req.json<Data>();
  const base64 = data.body;

  const type = detectType(base64);
  const body = Buffer.from(base64, "base64");

  const key = (await sha256(body)) + "." + type?.suffix;
  /* vvvvvvvvvvvvvvvvvvvvvvvvvv */
  await c.env.BUCKET.put(key, body, {
    /* ^^^^^^^^^^^^^^^^^^^^^^^^^^ */
    httpMetadata: { contentType: type.mimeType },
  });

  return c.text(key);
});

TypeScriptを使うとよりDX高いです。 @cloudflare/workers-typesにはもうすでにR2のタイプがあります。 冒頭で、KVのバインディングとBasic認証で使うUser/Passを含めたEnvを書きました。

interface Env {
  BUCKET: R2Bucket;
  R2_IMAGE_KV: KVNamespace;
  USER: string;
  PASS: string;
}

BUCKETR2Bucket型が定義されて、補完が効きます。

また拙作のHonoでは、Hono<Env>とEnvをGenericsで渡せばc.env自体にも型がつきます。

このように、ほとんどゼロコンフィグで、Workersから操作できるのがリッチな開発体験になります。

3. Cloudflareの他の製品との組み合わせ

ただ、わりと通信が遅い。今回の場合上りは許容できるとしても、下りが遅い。 適当な画像をアップロードして表示させると、手元で「1s」以上かかって遅い。ベータ版だから目を瞑るとしても、どうにかしたい。

そこで、Cloudflare KVを使ってキャッシュしてみます。 KVも似たようにWorkersスクリプトから操作できるので、こんなコードが書けます。

ファイルパスをキーとして、KVからメタデータ(MIMEタイプ)付きで値を取得。 データがあれば、そのままそれを使用。 なければ、バケットからgetして、その値をKVにpushしつつデータとして返却という、 「いかにも」なロジックです。

app.get("/:key", async (c) => {
  const key = c.req.param("key");

  /* vvvvvvvvvvvvvvvvvvvvvvvvvv */
  const res = await c.env.R2_IMAGE_KV.getWithMetadata<MetaData>(key, {
    type: "arrayBuffer",
  });
  /* ^^^^^^^^^^^^^^^^^^^^^^^^^^ */

  let data: ArrayBuffer = res.value;
  let contentType: string = "";

  if (data) {
    contentType = res.metadata.contentType;
  } else {
    const object = await c.env.BUCKET.get(key);
    if (!object) return c.notFound();
    data = await object.arrayBuffer();
    contentType = object.httpMetadata.contentType;
    c.event.waitUntil(
      /* vvvvvvvvvvvvvvvvvvvvvvvvvv */
      c.env.R2_IMAGE_KV.put(key, data, {
        metadata: { contentType },
      })
      /* ^^^^^^^^^^^^^^^^^^^^^^^^^^ */
    );
  }

  return c.body(data, 200, {
    "Content-Type": contentType,
  });
});

ね、R2もKVも同一に扱えて「プログラマブル」なんすよ。 ちなみに、KVでキャッシュすることで、返答速度が「x0ms」単位まで短くなりました。

KV、そして時を同じくして発表された「D1」を含めるとCloudflareだけミドルウェアがたくさんあります。

  • Cloudflare Workers - エッジファンクション
  • Cloudflare Pages - 静的コンテンツ配信
  • KV - KVストア
  • Durable Object - ステート管理
  • R2 - ストレージ
  • D1 - SQLデータベース

さらに、Cloudflare Workers同士を結びつける「Service Bindings」や、トンネリングのcloudflaredなんかもあります。お金を少し払えばCache APIImage Resizerも使えます。 これらがエッジで動いちゃう。

WorkersとKVとDOだけでは弱いと思っていたところ、ストレージとデーターベースが今回追加されて、もうCloudflareだけでフルスタックですねこれ。しかもそれらのほとんど全てをWorkersでJavaScript APIとして操れて、Wranglerという統合環境があるという。


さぁ、回し者のみたいになってきたのでまとめます。

まとめ

以上、「R2もいいぞ!」でした。 まぁ、最終的には「Cloudflareいいぞ!」ってなってますが、「R2いいぞ!」とか「D1よさそう!」っていうのはそういうことなんだと思います。 R2はオープンベータで誰でも使えるので試してみるといいです! 今回作ったr2-image-workerのリポジトリはこちらです。

あと、HonoというCloudflare Workers向けのWebフレームワークを作ってるのでよろしく。