しばらくの間、画像ファイルをプロジェクトの中にそのまま置いていました。心理テスト結果画像、ゲームのサムネイル、各種アイコンが`public/images`フォルダに少しずつ積み重なっていくうちに、リポジトリの容量が重荷に感じられてきました。クローンするだけで時間がかかるし、CI/CDパイプラインもなんとなく重い。いつかは分離しなければと思いつつ先延ばしにしていましたが、結局自分で手をつけました。
R2を選んだ理由:コスト比較と同じエコシステム
ストレージ候補としてはAWS S3、Cloudinary、Cloudflare R2あたりが思い浮かびました。それぞれなんとなくは知っていましたが、実際に使ったことがないのでコストや上限がいまいちピンと来なかったんです。そこで今回もGeminiに聞いてみました。各サービスのメリット・デメリットとだいたいのコスト構造をまとめてもらったら、小規模なサイドプロジェクト基準ではR2が最も安い選択肢だという結論が出ました。
それに、すでにCloudflare Pagesでデプロイしていました。同じCloudflareのダッシュボードで管理できれば設定も自然につながりそうだし、同じネットワーク内でコンテンツを配信するので遅延も減りそうでした。決定はすぐに出ました。
設定と移行作業:スクリーンショット+AI+スクリプト
R2バケットの作成から公開ドメインの設定まで、3話のCloudflare Pages、5話のFirebaseの時と同じ方式で進めました。ダッシュボード画面をスクリーンショットで撮って、Geminiに「ここで次に何をすればいい?」と聞きながら一つずつ進める方式です。こうすれば初めて使うサービスでも詰まることなく設定できます。AIがあれば大抵のことはできる時代だなと、今回も改めて実感しました。
問題は画像を移す作業でした。`public/images`にかなりのファイルが溜まっていて、一つずつアップロードしていたら時間がかかりそうでした。Geminiに一括アップロードスクリプトを作ってもらったら、R2 APIを使ってフォルダ構造そのままアップロードするスクリプトを作ってくれました。スクリプトを実行した後、コード内にハードコードされていた画像パスをR2のURLに一括置換する作業も同様に頼んで処理しました。
予想外の問題
移行作業自体は思ったより早く終わりました。でもその後に細々した問題がありました。一つ目はローカル開発環境で画像が表示されない問題でした。R2のCORSポリシーに`localhost`が含まれていなかったのが原因でした。CORSポリシーに`localhost`を追加して解決しました。
二つ目の問題は、心理テスト結果の画像保存機能で発生したCORSキャッシュ問題でした。結果カードを画像として保存する機能がR2移行後に突然エラーを出し始めました。厄介だったのは、ブラウザが古いCORSレスポンスをキャッシュしていたため、設定を変更してもキャッシュが生きている間はエラーが続いたことです。この部分はAIが原因を特定して解決まで手伝ってくれました。
プロジェクトが軽くなった
R2に移行してから一番最初に実感したのはクローン速度でした。画像ファイルがリポジトリから抜けたことで、全体の容量が目に見えて減りました。CI/CDも少し軽くなった感じがしました。小さな違いかもしれませんが、開発中に画像のせいで無駄に待たされることがなくなったのは、やはり快適でした。
最初からR2を使っていればよかったのですが
最初から画像を外部ストレージに置いていれば、Gitの履歴はずっときれいだったでしょう。でも、Cloudflareを初めて使ったし、コスト面でどこを使うべきか最初はわかりませんでした。サイドプロジェクトをしばらく動かしてみて、コスト構造を実際に経験してから比較できるようになったので、最初からR2を選べなかったのは仕方なかったと思っています。
今からでもR2に移行してプロジェクトを軽量化できたのは良かったと思っています。サイドプロジェクトとは元々そうやって作りながら直していくものだと思うことにしました。履歴に残った画像は、ただ当時の痕跡だと思っています。
次に新しいプロジェクトを始めるなら、画像ストレージは最初から分離しておくと思います。今回の経験はそれくらいの教訓を残してくれました。