🍆

Cloudflare ImagesのDirect Creator Uploadを使ってみた

2022/05/26に公開

Cloudflare Imagesというものを使ってみました。
使ってみた結果、とてもシンプルで使い勝手が良さそうな印象でした。

AVIF形式で配信してくれます。非対応ブラウザではちゃんと従来の画像形式で配信してくれました。優秀!

※Next.js + Supabaseという構成で開発中。

2022/07/31追記:
Cloudflare Imagesは便利な印象でしたが、表示にワンテンポ遅れる傾向にあるためちょっとストレスを与える原因になりそうだと感じました。このワンテンポが割りと閲覧に支障が出そうな感じでして、それを加味するとコストが高く感じました。加工だけCloudflare ImagesにしてGoogle Cloud Storageに自動コピーする仕組みを考えましたが、仕様でガッチリガードしてあり難しそうでした。(多分、この対策の処理が入ってて遅いのかも?)ということで、加工はクライアント側でどうにかし、Google Cloud Storageにアップロードすることにしました。(Google Cloud StorageにもDirect Creator Uploadと同じような仕組み = 署名付きURL にて直接アップロードできます。)

Cloudflare Imagesって何?

  • リサイズ、最適化が簡単にできる。
  • 料金がお安い?(オリジナル画像10万点あたり5ドル、画像配信10万点あたり1ドル。リサイズや最適化で追加料金が発生しないらしい。イグレスコストって言うらしい)
  • バリアント(variants)という仕組みで大量にいろんなサイズの画像を発行できる。

利用料金は必ず毎月5ドル~課金されます。この中には先で挙げた10万点分の使用が含まれている様子。超えた場合はそれに応じて後払いで課金されるそうです。

Next.js + Supabaseで開発をしているのですが、Vercel等のサーバーレス構成でデプロイすることを想定すると、「帯域幅」にはかなり気を使いたいところです。Supabaseではメディアファイルの送受信は極力0にしていきたく。そんな中で Cloudflare Images は帯域幅ではなく、配信数でカウントしてくれるので恐らく個人開発者を助けてくれる最高の存在なのではないでしょうか。

個人開発者向けでCloudflare Imagesよりも素晴らしいサービスがあれば教えてください!

料金について

Cloudflare Imagesでは

  • 画像格納10万点あたり月額5ドル(変換後の画像はカウントされない。あくまでオリジナル画像のみの点数)
  • 画像配信10万点あたり月額1ドル(帯域幅bandwidthや最適化処理には課金されない。)

以上の課金システムになっています。

Cloudflare Imagesにどんな画像を格納しておくとお得感があるかを考えてみます。

◆ケース1:そもそも画像の加工不要
この場合は恐らく他のストレージサービスを利用した方がいいんじゃないかと思います。Cloudflare R2の無料枠が最強すぎると話題になっています。ただし、ベータ版ということもあってか下りに1sほどかかるレベルで遅いとの噂もあるのでよく検証した上で使用する必要がありそうです。そもそも、Workersを書いてWorkersの処理結果としてファイルを取得する、みたいな動きにしないとパブリックアクセス的な使い方はできないようです。Cloudflare R2は選択肢としては微妙な存在かもしれません。課金されること前提で定額で使いたいならDigitalOcean Spacesなどが安価でいいんじゃないでしょうか。もしくはGoogle Cloud Storageでもいいんじゃないでしょうか。2022年10月から値上げされますが、個人レベルのプロダクトならそれほど大きなダメージはないと思われます。

◆ケース2:加工必要で画像サイズが大きく、配信数がそれなりにある。
この場合はCloudflare Imagesが最適かと思います。帯域幅が課金対象になることはありませんから、Cloudflare Imagesに負担してもらうことでお得感があるんじゃないでしょうか。ただし、アップロード対象は10MB以下の画像ファイルですので、それを超える場合は別の手段を選択する必要があります。

◆ケース3:加工必要で画像サイズが小さく、配信数がかなり多いと想定。
この場合はCloudflare Imagesには適さないと思います。1ユーザーあたりの配信数が多くなってしまい、急激に課金される可能性が考えられます。代替としてCloudinaryがあります(25クレジット分無料。1クレジット=1000回の加工、1GBのデータ保存、1GBの帯域幅)25クレジット分に収まるならばCloudinaryを選択するのが無難かと思います。

Cloudinaryを使うと有料課金時には89ドルを支払うことになる可能性もあります。確実に25クレジット以下で抑えられるだろう、という自信が無ければギャンブルせずに素直にCloudflare Imagesを選んでおいた方が安心感あると思います。

Direct Creator Uploadって何?

Direct Creator Uploadはアップロード用のURLを発行し、そのURLに対してファイルをアップロードすることができるという仕組みです。これの良いところは、

  • APIキーやトークンがクライアントに露出しない
  • メディアファイルがサーバーを仲介しないので帯域幅の節約になる。

ではないでしょうか。
クライアント側がCloudflare Imagesに直接アップロードする形になるので、サーバー側での記述もほとんど必要なくなります。

とはいえ、アップロード用のURLを発行するためには、自分のサーバー側でCloudflare Imagesにリクエストを投げる必要があります。サーバーが間に入ってメディアファイルのやり取りをすることを考えればかなりマシだと思います!

Next.jsのAPI Routesでアップロード用のURLを発行できるようにする

※型の処理とかは全然何もしてないので何卒...。

Form-Dataをインストールしていない場合は

npm i form-data

でインストールしておきましょう。

/api/image/upload-profile.ts
import FormData from 'form-data'
import type { NextApiRequest, NextApiResponse } from 'next'

const ImageUploadProfile = async (req: NextApiRequest, res: NextApiResponse) => {
  const formData = new FormData()
  formData.append('requireSignedURLs', 'true')

  let results = {
    code: 400,
    uploadURL: null,
    message: '処理が正しく完了しませんでした。'
  }

  if (req.method !== 'POST') {
    return res.status(400).json(results)
  }

  try {
    const response = await fetch(
      'https://api.cloudflare.com/client/v4/accounts/<ここにアカウントIDを入れる>/images/v2/direct_upload',
      {
        method: 'POST',
        headers: {
          Authorization: 'Bearer <ここにAPIトークンを入れる>'
        },
        body: formData
      }
    )

    const cloudflareImages = await response.json()

    if (cloudflareImages.success) {
      results.code = 200
      ;(results.uploadURL = cloudflareImages.result.uploadURL),
        (results.message = 'アップロードURLが正常に発行されました。')
    }
  } catch (e) {}

  return res.status(200).json(results)
}

export default ImageUploadProfile

これで /api/image/upload-profile をPOSTで叩くと

こんな感じでアップロードURLが入ったデータが返ってきます。
このアップロードURLは有効期限がありますので隠す必要はありませんが心配性なので一応。

画像をアップロードしてみる

クライアント側の処理

async () => {
  const blob = 'ここには画像パスが入っていると思ってください'
  const responseUploadURL = await fetch('/api/image/upload-profile', {
    method: 'POST'
  })
  const dataUploadURL = await responseUploadURL.json()

  // アップロードURLを格納
  const imageUploadURL = dataUploadURL.uploadURL

  const formData = new FormData()
  formData.append('file', blob)
  const cloudflareResponse = await fetch(imageUploadURL, {
    method: 'POST',
    body: formData
  })

  console.log(await cloudflareResponse.json())
}

アップロード処理前に、先程のAPIを叩いてアップロードURLを発行します。その後にアップロードしたい画像のパスを入れてfetchでPOSTするだけでOKです。

アップロードが完了するとこんなレスポンスが返ってきます。
あとはCloudflareのダッシュボードの「イメージ」欄で正常にアップロードができていることを確認して終わりです。


+知識

Next.jsで呼び出すときはnext/imageを使わない。

next/imageを使ってしまうと、Next.jsをホスティングしているサーバーで画像の最適化処理が行われてしまう。これでは帯域幅の節約にはならない。生HTMLのimgタグを使用しましょう。

Cloudflare ImagesのバリアントのFit値は色々ある

バリアントを設定しておくと、その設定に応じた画像に加工されます。例えば、CSSでもお馴染みのcontainな感じにしたり、coverな感じにできたりします。Cropもあるのですが、Positionなどを指定する形では処理できない(?)っぽいです。

個人的には react-avatar-editorで画像を切り抜き、その結果として出力できるblobデータをCloudflare Imagesに投げて画像データ化することで活用しています。(クライアント側で不正な動きをしていた場合にCloudflare Imagesがいい感じに処理してくれることを願ってる。丸投げで楽しました。)

下記はFit値のドキュメントとDeepLによる翻訳です。

https://developers.cloudflare.com/images/cloudflare-images/transform/resize-images/

▼ Scale down
画像は、指定された幅または高さに完全に収まるように縮小されますが、拡大されることはありません。

▼ Contain
画像は、縦横比を保ったまま、指定された幅または高さ内で可能な限り大きくなるようにリサイズ(縮小または拡大)されます。

▼ Cover
画像は、幅と高さで指定された領域全体を正確に埋めるようにリサイズされ、必要に応じてトリミングされます。

▼ Crop
画像は、幅と高さで指定された範囲に収まるように縮小・トリミングされます。画像は拡大されない。指定された寸法より小さい画像に対しては、scale-downと同じです。指定された寸法より大きい画像については、coverと同じです。

▼ Pad
画像は縦横比を保ったまま、指定された幅または高さ内で可能な限り大きくなるようにリサイズ(縮小または拡大)され、余分な領域は背景色(デフォルトでは白)で塗りつぶされます。

Discussion