Cloudflare Images を画像ストレージ・変換ソリューションとして使う
テキストは数 KB に過ぎないが、未処理の画像は平気で数 MB に達する。ウェブページの読み込み速度を上げるため、ここ数年ブラウザは多くの努力を重ねてきた。たとえば WebP や AVIF フォーマットを通じて、より優れた圧縮率でファイルサイズを小さく抑えるといったことだ。これにより、実質的に転送速度が向上する。
JPEG は離散コサイン変換を用いて人間の目に感知しにくい高周波のディテールを切り捨て、大幅なファイルサイズ削減を実現している。非可逆圧縮ではあるものの、見た目にはほとんど違いがわからない。
近年の WebP や AVIF は圧縮率をさらに一段押し上げたが、その代償として一部の古いブラウザでは対応していないため、<picture> を使ってフォールバックを用意する必要がある:
<picture>
<source srcset="hero.avif" type="image/avif" />
<source srcset="hero.webp" type="image/webp" />
<img src="hero.jpg" />
</picture>
ブラウザは上から順に対応している最初のフォーマットを選んでレンダリングする。width/height、srcset、sizes、アートディレクションについては、別の記事により詳しく書いたので参考にしてほしい。
この記事で話したいのは、そのもう半分だ。これら多様なフォーマットやサイズの画像は、一体どこからやってくるのか?
ユースケースに応じて、ウェブ上でより良い体験を提供するために、通常は画像に対して以下のような処理を行う:
- リスト用サムネイル、詳細ページ用の中画像、大画像へのリサイズ
- 各サイズごとにそれぞれ AVIF、WebP、JPEG を生成
- デバイスの画面幅や DPR に応じて実際に配信する画像を決定するか、フロントエンドで動的に選択
- これらのファイルをストレージに保存し、手前に CDN キャッシュ層を配置
固定数の静的ファイルであれば、ビルド時にあらかじめ必要な各フォーマットへ画像を変換しておけばよい。しかし UGC プラットフォームでは数をコントロールできず、ユーザーがアップロードした後にサーバー側で処理することになる。
これらの処理は一つひとつ分解すれば難しくないが、完璧にこなした上でトラフィックに耐えられるようにするのが難しい。この2つが重なると、コストが釣り合わないほど跳ね上がる。問題の核心は、画像変換が CPU バウンドなタスクであるという点だ。
AVIF を例に挙げよう。AVIF は AV1 動画コーデックのキーフレームから派生したフォーマットであり1、動画エンコードは本質的に「エンコードは非常に高コスト、デコードは非常に低コスト」にできている。再生時のデコードをスムーズにするため、意図的に CPU コストをエンコード側に寄せているのだ。
AVIF はファイルサイズが非常に小さく、ブラウザのデコードも高速で、画質も相対的にはるかに優れている。だが、1枚の画像を AVIF に圧縮するのは極めて CPU を消費する。Jake Archibald の実測によれば、libavif の最高 effort パラメータを使った場合、1枚の画像を圧縮するだけで10分以上かかることもあるという1。
そのため、自身のサーバーで動的に画像を変換するという、一見もっとも直感的なアプローチは危険なものになる。画像が増え、ちょうどトラフィックのピークと重なると、これらの変換処理が CPU を一気に食いつぶし、他のビジネスロジックのコードを圧迫して、さらには OOM(Out of Memory)を引き起こしやすくなる。
Next.js の next/image のように、デフォルトでサーバーリソースを使ってオンデマンド変換を行うものもある(裏で動いているのは Sharp だ)。確かに極めて便利だが、それは CPU を大量に消費し、外部に公開された API をアプリケーションレイヤーに組み込んでいるのと同じだ。
公式ドキュメント自体でも qualities や remotePatterns のホワイトリストを設定するよう注意喚起されている。さもないと、悪意のある大量の画像変換リクエストによってメモリを枯渇させられる脆弱性になりかねない2。
**高トラフィックなシステムにおいて、CPU バウンドかつ大量のメモリを消費するタスクには細心の注意を払う必要がある。**トラフィックが増加すると、サーバーは簡単にボトルネックに達し、他のビジネスロジックのコードにまで悪影響を及ぼす。
では、Lambda や非同期処理を使って画像処理を別の場所にオフロードするのはどうだろうか?可能ではあるが、そうすると管理すべき複雑性が指数関数的に跳ね上がる。
AWS 公式が用意しているソリューション(以前は Serverless Image Handler と呼ばれ、現在は Dynamic Image Transformation for Amazon CloudFront に改称)3の構成を見るだけでも、どれほど沼が深いかがわかる。キャッシュ層としての CloudFront、エントリーポイントとしての API Gateway、Sharp で変換を実行する Lambda、元画像やログを置く S3、スマートクロップを行いたいなら Amazon Rekognition との連携、直リンク防止には Secrets Manager による署名が必要になる。この一つひとつが設定を要し、保守が必要で、料金が発生するものばかりだ。
そしてもう一つ、見落とされがちなコストがある。帯域幅だ。画像はトラフィックの大部分を占め、クラウドからの下りデータ転送(egress)には別途費用がかかる。
S3 から直接画像を外部配信するのは特にコストが高い。標準的なやり方は手前に CloudFront を挟み、キャッシュによってオリジンへのリクエストをブロックすることだ。しかし、生成する派生ファイルが増えるほど(サイズ × フォーマット)、キャッシュは分散する。キャッシュミスが発生すれば、S3 から再度読み出して変換し直さなければならない。帯域幅を節約するために用意したマルチフォーマット・マルチサイズが、皮肉にもキャッシュ効率を薄めてしまうのだ。
会社の規模がすでにその開発コストを許容でき、十分なビジネス上の戦略的意義がない限り、この手のものは自作しないのが最善の策だ。
Cloudflare Images がすべて解決してくれる
画像処理の要件がある場合、僕は現在、基本的にすべて Cloudflare Images を使っている。先ほど挙げたような面倒な問題は、すべてこいつが片付けてくれる。類似のサービスには BunnyCDN や ImageKit などもある。
オリジナル画像を1枚アップロードするだけでいい。異なるサイズやフォーマットが必要な場合も、事前に何十ものファイルを書き出しておく必要はなく、URL にパラメータを付与するだけで、Cloudflare がエッジノードでリアルタイムに変換してくれる。
Flexible variants を有効にすると、URL は次のようになる4:
https://imagedelivery.net/<account_hash>/<image_id>/w=400,quality=80
フォーマットのネゴシエーションは自動で行われる。配信 URL 経由でアクセスすると、Cloudflare はブラウザから送られる Accept ヘッダーを読み取り、AVIF に対応していれば AVIF を、WebP にしか対応していなければ WebP を返し、どちらも非対応の場合にのみ元のフォーマットへフォールバックする5。自分で <picture> を書いてフォーマットを並べる必要も、自分で判定するロジックを書く必要もない。
たとえば、ブラウザでこの画像のリンクを開いて Network タブを観察すると、ブラウザの対応状況に応じて AVIF または WebP が返されていることがわかる。しかし実際には、最初から最後までオリジナル画像が1枚あるだけで、僕は何の変換処理も行っていない。
よくある操作もすべて組み込まれている。リサイズ、トリミング(顔認識クロップを含む)、ぼかし、反転、回転、明るさやコントラストの調整などだ。
もちろんカスタムドメインも設定できる。僕の画像はすべて image.kalan.dev に置いており、そのドメインが同じ Cloudflare アカウントのゾーンにあれば利用可能だ6。
CDN キャッシュはデフォルトで有効だ。変換された画像はそのまま Cloudflare のエッジキャッシュに入り、同じ組み合わせ(元画像 + パラメータ)の2回目以降のリクエストは最寄りのエッジから返され、オリジンにリクエストが飛ぶことはない。
実は2通りの使い方がある。1つは画像を Cloudflare に直接保存する方法(Hosted Images)、もう1つは画像を自分のストレージ(R2 でも S3 でもよい)に残し、エッジでの変換機能だけを利用する方法(現在は Transformations と呼ばれる)だ。
料金体系もこれら2つに対応している。Cloudflare に保存する場合はストレージと配信の容量が加算され、変換機能だけを使う場合はユニークな変換回数のみがカウントされ、無料枠も用意されている7。現在の料金体系は以下のとおりだ:
| Metric | Pricing |
|---|---|
| Images Transformed | First 5,000 unique transformations included + $0.50 / 1,000 unique transformations / month |
| Images Stored | $5 / 100,000 images stored / month |
| Images Delivered | $1 / 100,000 images delivered / month |
また、ローカルから Cloudflare Images に画像をアップロードしやすくするための簡単な CLI ツールも作成した。必要であれば参考にしてほしい。
Footnotes
-
AVIF has landed — Jake Archibald。AVIF は AV1 のキーフレームから派生したフォーマット。記事内の実測では libavif の最高 effort 設定で1枚の画像を圧縮するのに10分以上かかる場合がある。 ↩ ↩2
-
next/image 公式ドキュメント。オンデマンド最適化、メモリ制限、
qualities/remotePatternsのホワイトリストに関する記述。 ↩ -
Dynamic Image Transformation for Amazon CloudFront(旧 Serverless Image Handler)。 ↩
-
Cloudflare Images pricing。実際の無料枠と料金は公式ページを参照。 ↩
関連記事
- 測定が目標になるとき:窓税からPull Request数まで かつて僕は小さなツールを自作し、四半期で自分がどれだけPRに貢献したか、レビューコメントをどれだけ残したか、チケットをどれだけ消化したかを集計して、上司にアウトプットを証明しようとしたことがある。上司は淡々と、評価はアウトプットだけで見るものではないと言った。数年後、僕はようやく理解した――測定が目標になるとき、それはもはや良い測定ではなくなるのだ。英国の窓税、ハノイのネズミ駆除の報奨金から、現代のPR数による開発者評価に至るまで、そのメカニズムはまったく同じだ。
- もう AWS Access Key を使うのはやめよう Access Key は AWS において見落とされがちなセキュリティリスクだ。OIDC と IAM Role を組み合わせることで、GitHub Actions にシークレットを一切保持させることなく、安全に AWS リソースを操作できるようにする。
- データベース主キー:AUTO_INCREMENT、UUID、そしてUUIDv7 バックエンド開発で度々直面する主キーの決定。auto incrementを使うべきか、それともUUIDか?衝突への懸念は?UUIDv7とcreated_at + インデックスの性能差はどれほどか?実際に2,000万件のデータで検証したベンチマークと設計上の意思決定を解説する。
- Zeaburを使ってみた感想とレビュー 個人開発者がサービスをデプロイする際、一般的にはVercelなどのプラットフォームを選ぶことが多いが、データベース接続などもう少し高度な要件が必要になるとVercelはそこまで便利ではなくなる。一方で一般的なクラウドプロバイダーの価格は個人開発にとってはかなり高額だ。この記事ではZeaburを使ってみた感想をシェアし、おすすめしたいと思う!