Skip to content

ADR 0003: 統合 CMS のメディアストレージに Cloudflare R2 を採用する ​

コンテキスト ​

統合 CMS (Strapi 5) で扱うメディア (記事のカバー画像 / OG 画像 / 本文内画像) の保存先を決める必要がある。Linear の初期方針は「GCS (uzu-shared バケット)」だったが、以下 2 つの実装上の問題が判明した:

  1. 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 の現行運用と衝突する。
  2. 公式 provider (@strapi/provider-upload-aws-s3) が利用可能なのは S3 互換ストレージのみ。GCS の S3 互換 API を経由する場合は HMAC キーが必要で、その時点で Workload Identity 連携のメリットが消える。

検討した選択肢 ​

案StorageProvider認証Workload IdentityEgressメンテ
A. GCS (uzu-shared) + community providerGCS@strapi-community/...-google-cloud-storageSA / WI✅通常community
B. GCS (uzu-shared) + S3 互換 + 公式 S3 providerGCS@strapi/provider-upload-aws-s3HMAC❌通常公式
C. Cloudflare R2 + 公式 S3 provider (採択)R2@strapi/provider-upload-aws-s3HMAC❌無料公式

決定 ​

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-s3 provider に 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 更新を追加する