Discourse マルチサイト環境で Cloudflare R2 を 1 サイトのみに適用する
マルチサイト 構成の Discourse を運用していて、1 サイトだけ を Cloudflare R2 に移行したい場合(アップロード、バックアップ、および任意の静的アセット)、標準的な アップロード用の S3 互換オブジェクトストレージプロバイダーの構成 ガイドでは、マルチサイト特有の複雑さを十分にカバーしていません。この記事では、実際に何が起きるのか、何がサイト単位で何がクラスタ全体に適用されるのか、そして実際に遭遇し、解決に時間を要したいくつかの落とし穴について解説します。
理解すべき核心:GlobalSetting と SiteSetting の違い
この設定はすべて、以下の 1 つの区別に基づいています。
app.ymlの環境変数 (DISCOURSE_*) はGlobalSettingとなります。コンテナ起動時にプロセス環境から一度読み込まれ、クラスタ内のすべてのサイトで共有されます。RAILS_DBはこれらに影響しません。- 管理画面のフィールド は通常の
SiteSettingです。各サイト固有のデータベースに保存され、本当にその 1 サイトのみにスコープが限定されます。
ある項目に GlobalSetting が存在する場合、管理画面 UI の対応する SiteSetting フィールドは黙って上書きされ、非表示になります。つまり:app.yml に設定した内容は例外なくすべてのサイトに適用され、RAILS_DB による回避策はありません。
パート 1 — アップロードとバックアップ(本当にサイト単位、簡単)
この部分は、期待通り完全に機能します。enable_s3_uploads、s3_upload_bucket、backup_location、s3_backup_bucket、および認証情報フィールドはすべて通常のサイト設定です。管理 → 設定 → 「S3」を検索 して設定し、R2 に移行したい特定のサイトにログインした状態で操作し、app.yml は触らないでください。クラスタ内の他のサイトはローカルに保存し続けます。
R2 用のサンプル値:
Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <あなたの R2 トークン>
S3 region = auto
S3 upload bucket = <バケット名>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false (R2 はオブジェクト ACL ではなくバケットレベルの権限を使用します)
S3 backup bucket = <バックアップバケット名>
Backup location = S3
バケットの CORS ポリシーは Cloudflare ダッシュボードで直接設定してください(R2 は Discourse の CORS rake タスクを必要としません):
[
{
"AllowedOrigins": ["https://your-site.tld"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag"],
"MaxAgeSeconds": 3000
}
]
パート 2 — 既存のローカルアップロードの移行
rake uploads:migrate_to_s3 は環境変数から S3 設定を読み取るのみです。管理画面 UI で何を設定していても、サイト設定へのフォールバックは一切ありません。これは設定ミスではなく、タスク自体の実際のギャップです。app.yml を変更せずに、ワンオフ実行のために認証情報をインラインで渡してください:
./launcher enter app
RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<bucket name> \
DISCOURSE_S3_ACCESS_KEY_ID=<key> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<secret> \
rake uploads:migrate_to_s3
これらの環境変数はそのシェルプロセスの存在する間のみ有効であり、終了すると何も永続化されません。
新しめの AWS SDK バージョンでのチェックサムエラー
以下のようなエラーに遭遇した場合:
Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.
これは、最近の aws-sdk-core バージョン(デフォルトで CRC32 チェックサムを送信する)と R2 の間の既知の非互換性です。同じコマンドに以下の 2 つの環境変数を追加することで修正できます:
export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required
ほぼ成功した実行後の「未移行」レコードの残存
タスクが 1 of 1291 uploads are not migrated のようなメッセージで終了した場合、パニックにならずにください。それ以外はすでに移行済みで、DB の URL も書き換えられています。rails c で遅れたレコードを見つけます:
base_url = File.join(SiteSetting.Upload.s3_base_url, "original/")
Upload.by_users.where("url NOT LIKE '#{base_url}%'").pluck(:id, :url, :original_filename)
私のケースでは、バックアップログの zip ファイルが、チェックが期待する CDN URL 形式ではなく、R2 の生エンドポイント形式の URL を使用していたため、Upload レコードが 1 件残っていました。これは実際の失敗ではなく、偽陽性です。意味のあるコンテンツでない場合は、URL を修正するかレコードを削除してください。
パート 3 — 静的アセット(JS/CSS)— 本当にクラスタ全体に適用される部分
ここが「1 サイトのみ」を目指す上で壁にぶつかる場所です。コンパイルされたアセットはマルチサイトクラスタ全体で共有されます。コンパイルされた JS/CSS バンドルは 1 つであり、サイトごとに存在するわけではありません。アセットが R2 から配信されるかローカルから配信されるかは、GlobalSetting.use_s3? を介して Rails 起動時に一度だけ決定されます。サイトごとのオーバーライドは存在しません。
アセットを R2 にオフロードしたい場合は、接続詳細(アップロード有効化フラグではありません)を app.yml に設定する必要があります:
env:
DISCOURSE_USE_S3: true
DISCOURSE_S3_REGION: auto
DISCOURSE_S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
DISCOURSE_S3_ACCESS_KEY_ID: "xxx"
DISCOURSE_S3_SECRET_ACCESS_KEY: "xxx"
DISCOURSE_S3_BUCKET: <bucket name>
DISCOURSE_S3_CDN_URL: https://uploads.yourdomain.com
AWS_REQUEST_CHECKSUM_CALCULATION: when_required
AWS_RESPONSE_CHECKSUM_VALIDATION: when_required
hooks:
after_assets_precompile:
- exec:
cd: $home
cmd:
- sudo -E -H -u discourse bundle exec rake s3:upload_assets
- sudo -E -H -u discourse bundle exec rake s3:expire_missing_assets
注意事項:
DISCOURSE_CDN_URLは設定しないでください。DISCOURSE_S3_CDN_URLのみ設定してください。メインドメインが Cloudflare を経由してプロキシされている状態で両方を設定すると、メイン S3 ガイドの警告通りリダイレクトループが発生します。bundle exec rakeを使用し、bundle rakeは使わないでください(よくあるタイポです)。また、sudo -E -H -u discourseを使用してください(-Hはdiscourseユーザーに対してHOMEを正しく設定します。これがないと、Bundler は毎回一時ディレクトリにフォールバックします)。- アセットのオフロードは両方のサイトに影響します。 2 番目のサイトの
<script>/<link>タグも、同じコンパイル済みバンドルであるため、R2 の CDN URL を指し始めるようになります。バケットの CORSAllowedOriginsにすべてのサイトのドメインが含まれていることを確認してください。 - これは 2 番目のサイトの実際のアップロードを S3 に強制するものではありません —
enable_s3_uploadsはアセット配信のGlobalSettingに独立した、真のサイト単位の設定のままです。再構築後、そのサイトの DB でrails c内でSiteSetting.Upload.enable_s3_uploadsを確認してください。
パート 3 — USE_DB_S3_CONFIG — 実際の動作(と非動作)
一部のコミュニティ設定(例:Bitnami のチャート)では、s3:upload_assets が環境変数ではなくサイト設定から認証情報を読み取るための方法として USE_DB_S3_CONFIG=true が参照されています。これはアップロードタスク自体には機能しますが、GlobalSetting.use_s3? を切り替えるわけではありません。このフラグは、レンダリング時にアセット URL が CDN に書き換えられるかを实际控制するものです。したがって、USE_DB_S3_CONFIG を使用してファイルを R2 に正常にプッシュしても、ページレンダリング時のチェックが「S3 が有効」を見ないため、サイトがローカルでアセットを配信し続けることがあります。アセットを実際に R2 から配信したい場合は、DB 設定の回避策ではなく、app.yml に本物の DISCOURSE_USE_S3: true と接続用の環境変数が必要です。
パート 4 — R2 に載らないもの、とその理由
フックが機能していても、s3:upload_assets がアップロードするのは Rails.application.assets.load_path(Rails の Sprockets マニフェスト)にあるものだけです。以下の 3 つのカテゴリは、そのパイプライン外側で生成されるため、このリストに決して現れず、ローカルディスク上に留まります:
- テーマ CSS — Sprockets ではなく、Discourse の
Stylesheet::Managerによってテーマ/カラースキームごとに動的にコンパイルされます。 theme-javascripts—ThemeJavascriptCompilerによるテーマごとのコンパイル済み JS。extra-locale/ ロケール JS ファイル —JsLocaleHelperによって生成されます。
これは設定の問題ではなく、これらは元々 Sprockets アセットではなかったため、これらを取り込む環境変数は存在しません。実際には、これはコアの Ember/ベンダー JS バンドル → R2 に正常にオフロード;テーマ CSS/JS とロケール → ローカルに留まり、アプリが直接配信、という状態を意味します。これは正常な動作状態であり、壊れた状態ではありません。
まとめ:実際にどこに何を設定するか
| 項目 | 場所 | スコープ |
|---|---|---|
enable_s3_uploads, s3_upload_bucket, backup_location, s3_backup_bucket |
管理画面 UI、サイトごと | サイト単位 |
認証情報 + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 |
app.yml、アセット CDN オフロードを希望する場合のみ |
クラスタ全体(回避不能) |
after_assets_precompile フック |
app.yml |
クラスタ全体 |
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION |
app.yml |
クラスタ全体(無害な SDK 動作フラグ) |
| バケットの CORS | Cloudflare ダッシュボード | アセットが共有される場合、すべてのサイトのドメインを含める必要がある |
アセット CDN オフロードを必要としない場合は、パート 3 を完全にスキップできます。app.yml を一切触らずに、完全に機能する、真のサイト単位 R2 設定(アップロード + バックアップのみ)を実行できます。