ADR 0003: 統合 CMS のメディアストレージに Cloudflare R2 を採用する
- ステータス: 採択
- 決定日: 2026-05-13
- 関連 Linear: MJP-1530 / プロジェクト 統合 CMS (Strapi) 移行
- 関連 ADR: [[0002-cms-body-format]]
コンテキスト
統合 CMS (Strapi 5) で扱うメディア (記事のカバー画像 / OG 画像 / 本文内画像) の保存先を決める必要がある。Linear の初期方針は「GCS (uzu-shared バケット)」だったが、以下 2 つの実装上の問題が判明した:
- Strapi 5 用の GCS upload provider (
@strapi-community/strapi-provider-upload-google-cloud-storage) は community メンテで、engines.nodeを<=22.xに固定している。本リポジトリは Node 24 固定 (.tool-versions) のため、engine-strict=trueの現行運用と衝突する。 - 公式 provider (
@strapi/provider-upload-aws-s3) が利用可能なのは S3 互換ストレージのみ。GCS の S3 互換 API を経由する場合は HMAC キーが必要で、その時点で Workload Identity 連携のメリットが消える。
検討した選択肢
| 案 | Storage | Provider | 認証 | Workload Identity | Egress | メンテ |
|---|---|---|---|---|---|---|
| A. GCS (uzu-shared) + community provider | GCS | @strapi-community/...-google-cloud-storage | SA / WI | ✅ | 通常 | community |
| B. GCS (uzu-shared) + S3 互換 + 公式 S3 provider | GCS | @strapi/provider-upload-aws-s3 | HMAC | ❌ | 通常 | 公式 |
| C. Cloudflare R2 + 公式 S3 provider (採択) | R2 | @strapi/provider-upload-aws-s3 | HMAC | ❌ | 無料 | 公式 |
決定
C (Cloudflare R2) を採用。以下を併せて確定する:
- バケット: 新規
uzu-cms-mediaを Cloudflare ダッシュボードで作成 (R2 は terraform 管理対象外) - HMAC キー:
backend/studio_editorで既に使っている R2 アカウントの HMAC キーを 流用 する。CMS 用に新規発行しない (Secret 管理対象を増やさない) - 環境分離: 単一バケット内で
cms/dev//cms/stg//cms/prd/の prefix で論理分離する - 公開ドメイン: Cloudflare R2 Custom Domain
cms-media.uzu-asset.comを bucket に紐付け、フロントは Strapi を介さず直接 CDN から配信する
根拠
編集 / 配信コスト
- 記事画像は web (apps/web / mdms.jp) に公開されるため、egress が多い
- R2 は egress 無料なので、長期で CMS のメディア配信コストが大きく下がる
- 他の GCS バケット (
uzu_scenarios_config等) は uzu アプリ内部のアセットで egress は限定的なので、CMS だけ R2 にすることに整合性がある
既存運用との整合
- 社内では既に R2 を採用済み (
backend/studio_editor/uzu_codeがシナリオアセット用バケットで利用) - 同じ HMAC キーを流用することで、追加で管理する secret がゼロ
- DNS / CDN / Access も Cloudflare に統一されており、メディア配信ドメインも同じ運用 (
uzu-asset.com配下) に乗る
公式 provider の安定性
@strapi/provider-upload-aws-s3は Strapi 公式パッケージで、Strapi 5 へのアップグレードに追随する- community GCS provider のように
engines.node制約で詰まる懸念がない
トレードオフ
- Workload Identity が使えない: GKE Pod は R2 アクセスに HMAC キーが必要。既存 R2 利用箇所と同じ方式 (cms-secret に env として注入) で運用する
- GCS との二重管理: uzu のメインアセットは GCS、CMS だけ R2 になる。lifecycle / 監視ツールが分かれるが、運用境界として CMS が独立しているので許容範囲
- HMAC キー流出時の blast radius: backend / studio_editor / cms が同じキーを共有するため、流出時の影響範囲が広い。ローテーション運用は引き続き手動 (Cloudflare ダッシュボード) で、その時に全サービスの Secret を入れ替える
影響を受ける後続タスク
- M1:
config/plugins.tsでaws-s3provider に endpoint=R2 を指定。k8s manifest の env にR2_BUCKET_NAME/R2_PREFIX/R2_PUBLIC_BASE_URLを設定し、HMAC キーは cms-secret から注入 - M2: 既存記事の画像移行は MDX → Strapi 投入時に R2 へアップロード ([[0002-cms-body-format]] の Strapi Blocks 変換と同じスクリプト内で処理)
- M3: フロント (apps/web / mdms.jp) は
cms-media.uzu-asset.comから画像を直接 fetch する - 運用: 既存 R2 HMAC キーのローテーション運用に CMS の Secret 更新を追加する