Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】How Rotating S3 Presigned URLs Silently Defeated Next.js Image Optimization

2026年10月10日に「Dev.to」が公開したITニュース「How Rotating S3 Presigned URLs Silently Defeated Next.js Image Optimization」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

S3の署名付きURLがNext.js画像最適化で費用高騰の原因に。セキュリティ上毎回変わるURLを最適化機能がキャッシュの識別キーとして扱い、同じ画像でも常に新規と判断。キャッシュが効かず高額請求となった。画像を安定した内部パスで提供するよう修正し解決。セキュリティとキャッシュの考慮が重要だ。

ITニュース解説

ある日、ウェブサイト運営を支えるVercelというプラットフォームから、システムチームのSlackチャンネルに緊急のアラートが届いた。それは「画像最適化機能の利用量が、契約プランの上限に近づいている」という内容だった。月間50,000枚のソース画像が上限であるにもかかわらず、過去24時間で230万枚もの画像が使用されたと報告されたのだ。画像処理のシステムも、契約プランも誰も変更していない状況で、唯一最近リリースされた変更は、ウェブサイトに新しい商品ギャラリーページが追加されたことだけだった。

システム担当者は、まず原因究明に乗り出した。最初の仮説は、悪意のあるボットがウェブサイトをスクレイピング(情報収集)し、商品画像を大量に要求しているというものだった。ギャラリーページは公開されており、画像への直リンクも容易だったため、この可能性は十分に考えられた。しかし、Vercelのログを確認すると、ほとんどのリクエスト元が自社のウェブサイトのドメインであり、リクエストの頻度も通常のアクセスパターンと一致していたため、ボットによるものではないとすぐに判断された。

次に考えられたのは、新しく追加されたギャラリーページ自体に問題があるというものだった。もしかしたら、このページが意図せず多くの画像バリアント(異なるサイズや形式の画像)を生成しているのかもしれない。担当者はページの変更履歴を調べたが、画像の表示方法を指定する設定は以前から変わっておらず、1ページあたり最大でも120枚程度の画像リクエストしか発生しない計算だった。実際のトラフィックを考慮しても、230万枚という数字には到底届かない。こうして、二つの有力な仮説はわずか15分で否定されてしまった。しかし、Vercelのダッシュボードに表示される利用量は増え続けていた。原因は、画像が多数表示されていることでも、ボットの攻撃でもなく、既存の画像が「どのようにカウントされているか」にあると推測された。

Vercelの画像最適化機能は、画像のリクエスト回数ではなく、「最適化が必要なユニークなソース画像」の数で課金される仕組みになっている。本来、一度最適化された画像は、ウェブサイトのエッジ(ユーザーに近いサーバー)にキャッシュとして保存される。これにより、同じ画像が再度リクエストされた際には、再処理することなくキャッシュから高速に提供され、費用も発生しない。健康なウェブサイトであれば、同じソース画像を何度も最適化することは稀であるため、費用は抑えられるはずだ。今回の問題は、このキャッシュがほとんど機能しておらず、230万枚もの画像が「ユニークなソース画像」として毎回処理されていることだった。

真の原因を突き止めるため、担当者は画像最適化機能が要求しているURLのログを詳しく調査した。すると、あるユーザーのアバター画像を例に取ると、わずか2分間の間に5回のページ読み込みで、同じ画像であるにもかかわらず、異なるURLがリクエストされていることが判明した。画像が保存されている場所(バケット)やファイル名(キー)は全て同じだったが、URLの末尾に付加されているクエリパラメータ(?以降の文字列)が毎回異なっていたのだ。特にX-Amz-Date(日付)やX-Amz-Signature(署名)といった部分が、リクエストのたびに変化していた。

この違いが何を意味するのか。担当者はまず、何らかの理由でキャッシュが強制的に削除されている可能性を疑った。しかし、完全に同じURLをリクエストした場合にはキャッシュが正常に機能し、画像が高速に返却されることが確認された。つまり、問題はキャッシュが削除されることではなく、「URLが常に異なるため、キャッシュが全く使われない」ことだと結論付けられた。Next.jsの画像最適化機能、そしてVercelが提供するこの機能は、リクエストされたURL全体(クエリパラメータを含む全て)をキャッシュを識別するためのキーとして使用していたのだ。

この問題の根源は、2週間前に遡る。セキュリティ監査(SOC 2)の結果、ウェブサイトで使用されているアバター画像や商品写真が、誰でもアクセスできる公開S3バケット(Amazon S3は、ファイルをクラウドに保存するサービス)に保存されていることが指摘された。セキュリティ上のリスクを解消するため、画像はプライベートなS3バケットへ移行された。そして、ウェブサイトから画像を表示する際には、サーバーサイドで一時的にアクセスを許可する「署名付きURL」を生成し、そのURLを画像の表示要素に渡すように変更された。この署名付きURLは、一時的なアクセス権限を示すために、発行されるたびに有効期限や署名情報(X-Amz-DateやX-Amz-Signatureなど)が更新され、それがクエリパラメータとしてURLに含まれる。これこそが問題だったのだ。

署名付きURLが生成されるたびにクエリパラメータが変わるため、Next.jsの画像最適化機能は、同じ画像であっても毎回「見たことのない新しい画像」として認識してしまい、キャッシュを使わずに毎回最適化処理を行っていた。この変更が導入された当初は、ウェブサイトのトラフィックが少なかったため、この非効率性による追加費用は目立たず、通常の使用量の中に隠れてしまっていた。誰も画像最適化機能のキャッシュヒット率を個別に監視していなかったため、問題は発覚しなかった。しかし、新しい商品ギャラリーページのリリースにより、1ページに40枚もの画像が表示されるようになったことで、この既存のバグが急速に増幅された。1枚の画像のキャッシュミスが40倍になり、それが膨大なトラフィックによって一気に月間の上限を突破してしまったのである。

解決策は、「アクセス制御」と「キャッシュキー」を分離することだった。署名付きURLはアクセスを認証する正しい方法だが、画像の同一性を識別する目的には不適切だった。そこで、画像リクエストはウェブサイト内部の安定したパス(例: /api/media/[assetId])を経由するように変更された。この内部パスへのリクエストを受け取ったサーバーは、ユーザーのアクセス権限をチェックし、問題なければプライベートS3バケットから画像をフェッチしてクライアントに返却する。この方法であれば、S3署名付きURLをクライアントに公開する必要がなく、署名関連の情報が外部に出ることもないため、キャッシュキーを安定させることができる。

<Image>コンポーネントに渡すURLは、署名付きURLから安定したパス(例: /api/media/${product.imageId}.jpg)に変更された。product.imageIdは画像が変更されない限り不変であるため、画像最適化機能のキャッシュキーも安定し、キャッシュが正常に機能するようになった。もちろん、新しいルートハンドラーでは、アクセス権限が毎回チェックされるため、セキュリティレベルが低下することはなかった。

この修正がデプロイされた後、日次ユニークソース画像数は230万枚から4万4千枚に劇的に減少した。しかし、問題が顕在化した24時間だけで、およそ1,840ドルの超過料金が発生していた。そして、このキャッシュミスのバグは、トラフィックの増加によって表面化するまで、実に14日間も潜在していたのである。

この経験から得られた教訓はいくつかある。第一に、署名付きURLのような、生成されるたびに内容が変わる要素(署名、タイムスタンプなど)を含むURLは、URL全体をキャッシュキーとするシステムにおいてはキャッシュを無効化してしまうということだ。セキュリティ修正が、予期せぬパフォーマンスの低下を招くこともある。セキュリティ監査はアクセス制御の問題を見つけることに特化しており、キャッシュの挙動まではレビューの対象にならないことが多い。

また、利用量の上限アラートは、月間合計だけでなく、日次の変化率も監視するべきだということも分かった。もし日ごとの異常値を検知する仕組みがあれば、今回の問題は2週間も早く発見できたはずである。最後に、認証はサーバーサイドで行い、クライアントサイドでは安定した識別子を使用することが重要だ。CDNや画像最適化機能、ブラウザなど、キャッシュされる可能性のあるURLが認証を必要とする場合、その認証プロセスはURL自体に含めるのではなく、安定したエンドポイントの裏側で行うべきだ。

結局のところ、画像ファイル自体は移動しておらず、問題はブラウザのネットワークタブ上では同じに見えるが、バイト単位では毎回異なるクエリ文字列にあった。画像最適化機能は、同じ画像だと証明できるものをキャッシュするように設計されていたが、その証明の機会が与えられなかったために、その能力を最大限に発揮できなかったのである。

関連コンテンツ

関連IT用語