環境戦略 (dev / stg / prd)
UZU は GCP プロジェクト単位で 3 環境を分けている。本ドキュメントは「どこまで本番同等構成を取るか、どこはコストや運用簡素化のため割り切るか」のチーム合意を残し、議論が蒸し返されないようにするためのもの。
基本方針
"本番同等性" は絶対原則ではなく、目的に応じて選択する。 各インフラ要素ごとに以下 3 段階のいずれかを選ぶ:
| レベル | 意味 | 例 |
|---|---|---|
| F. フル同等 | prd と同一構成・同一データ | Firebase Auth、Redis、GraphQL schema |
| D. 縮小同等 | 構成は同じだが規模・データ量を削減 | Postgres / AlloyDB (後述) |
| S. prd 共有 | dev/stg は prd のリソースを cross-project 参照 | BigQuery のアプリ読み取りデータ |
判断基準:
- 書き込みがあるか(あるなら環境分離は必須)
- 再現性の価値(本番障害の再現、破壊的実験の必要性)
- コスト
- 運用負荷(Datastream のような daily DB reset と衝突する技術の扱い)
インフラ要素ごとの選択
Postgres / AlloyDB(アプリ DB)
Level: D. 縮小同等
| 環境 | 実体 | 備考 |
|---|---|---|
| dev | Cloud SQL (uzu-postgresql in uzu-dev) | AlloyDB ではなくコスト重視の Cloud SQL |
| stg | AlloyDB (uzu-shared in uzu-stg) | prd 同等 |
| prd | AlloyDB |
日次データ同期: .github/workflows/db_import_backend.yml が 05:00 JST(20:00 UTC)に prd dump を匿名化して dev/stg に import:
prd dump → postgresql_anonymizer でマスキング → (dev は sampling 縮小) → drop database app → importマスク範囲: backend/scripts/mask_rules.sql で定義。PII(email / 住所 / 支払情報 / chatroom 本文 等)のみ書き換え。PK / FK は prd と同じ値のまま保持される。つまり users.id=12345 は prd と dev で同一ユーザー(ただし dev の sampling で一部が抜ける)。
BigQuery
Level: 用途ごとに分かれる
BigQuery は一つの dataset を複数用途で使わない。以下 3 種類に分類する:
| 用途 | 例 | Level | 書き込み元 |
|---|---|---|---|
| アプリ読み取り元(学習・分析用) | uzu-prd.app_public.*、uzu-prd.mdmsjp_public.* | S. prd 共有 | Datastream (prd AlloyDB → prd BQ) |
| 行動ログ / イベントログ | mobile_events.scenario_views、mobile_events.recommendation_assignments 等 | F. フル同等 | 各環境のクライアント / backend |
| KPI / 分析用中間テーブル | app_kpi、app_opt 等 | F. フル同等(prd のみ運用) | prd のバッチ |
なぜアプリ DB データ (app_public) は prd 共有で OK か
- BigQuery は OLAP 設計で cross-project read 想定。Postgres のようにプロセス単位の接続ではなく、IAM 単位の read-only SELECT。dev が prd BQ を参照しても prd Postgres や prd アプリには一切影響しない
- ID 整合性がすでに取れている。dev/stg は prd の匿名化ダンプを毎日 import しているので、
user.id/scenario.idは prd と一致。dev Postgres の ent 行と prd BQ の該当行は同一主体 - コスト差はあるが決定打ではない
- S の場合: dev/stg からの BQ クエリ課金のみ(月 $0-5、on-demand 1 TiB 無料枠内で収まる規模)
- F の場合: 環境ごとに Datastream + BQ storage が必要。dev/stg も毎日
drop databaseがあるので replication slot が毎日吹き飛ぶ → Datastream 再初期化 + backfill が毎日発生するなど運用破綻
- 書き込み事故のリスクはゼロ。IAM で dataset level の
roles/bigquery.dataViewerのみ付与する
なぜ行動ログ (mobile_events.*) は環境別か
- 書き込みがある → 環境混在は禁止(dev テストデータが prd KPI を汚染する)
- A/B 実験割付 は環境ごとに独立した ID 空間を持つ必要がある
- PII 流出リスク軽減: dev クライアントから生の PII が prd BQ に流れない
実装上のルール
Backend のレコメンデーション学習ジョブ(spec)は:
| 環境変数 | 値 | 用途 |
|---|---|---|
GCP_PROJECT_ID | 各環境(uzu-dev / uzu-stg / uzu-prd) | 書き込み先(mobile_events.recommendation_assignments)および AlloyDB/pgvector 接続 |
RECOMMENDATION_BQ_READ_PROJECT | 全環境で uzu-prd | 読み取り元(app_public.*、学習データソース) |
この分離により、dev の学習ジョブでも本番データで実際に動作検証できる。
Redis / AlloyDB pgvector(推論の state)
Level: F. フル同等
scenario_embeddings / user_embeddings は各環境の AlloyDB に日次学習で書き込む。dev でも学習ジョブが動く(BQ は prd 共有で読むが、pgvector への書き込みは環境別)。
Firebase Auth
Level: F. フル同等
各環境で独立 Firebase プロジェクト。テストユーザーは環境ごとに作る。
Cloud Tasks / Pub/Sub
Level: F. フル同等
各環境で独立エンドポイント。
Cross-project 参照の IAM 原則
dev/stg の SA(サービスアカウント)が uzu-prd の BigQuery を read する場合:
- 付与する権限は dataset level の
roles/bigquery.dataViewerのみ(project 全体には付けない) - 対象 dataset は
uzu-prd.app_publicとuzu-prd.mdmsjp_publicに限定 - 書き込み権限は絶対に付与しない(IAM レベルで保証)
- 設定は Terraform
infra/terraform/modules/iam/に集約する
破壊的実験の原則
「prd データを破壊する実験」は dev/stg では不可能になる(S レベルの共有のため)。それでも問題ない理由:
- スキーマ変更は ent migration で PR レビュー経由
- 破壊的な SQL を試したいときは dev Postgres ローカルコピーで試す
- BQ スキーマを壊す実験は BQ の sandbox dataset を別途作る
変更時の参考
この方針を変える場合:
- 影響範囲: backend 学習ジョブ / BI ダッシュボード / KPI 集計クエリ
- 移行手順: Datastream 新設 → dual-read 期間 → 旧ソース廃止
- 過去の議論: レコメンデーション spec の §8 および関連 PR
FAQ
Q. dev でも prd 同等の Datastream を立てたら整合性が綺麗では?
- 毎日の
drop database appで replication slot が消える → stream が毎日 FAILED → 再初期化運用 - 再初期化のたびに backfill が発生 → 月額コストが不定に跳ねる
- メリットより運用負荷が大きいため却下(2026-04 合意)
Q. prd BQ が落ちたら dev も落ちる?
- Yes、ただし稀。BigQuery 自体の SLA は 99.99%
- dev で学習ジョブが失敗しても dev アプリは継続動作する(pgvector の古い embedding が残っているため)
Q. 個人情報の扱いは大丈夫?
- dev SA は
uzu-prd.app_publicの read only - 匿名化は Postgres dump 時点で実施済み
- さらに詳細な取り扱いは別途 security doc で管理