Appearance
開発フロー (staging への反映 / マイグレーション / 巻き戻し)
「コードを書いてから staging に反映されるまで」と「DB を変えるとき」の手順。
結論サマリ
apps/webだけ変更 →mainpush で Netlify が auto deployapps/apiだけ変更 →mainpush で GHA (deploy-staging.yml) が Cloud Run へ deploypackages/*だけ変更 → Cloud Run には反映されるが Netlify は build skip するので、netlify deploy --build --prodを手動で叩く- DB スキーマ変更 →
pnpm -F @liga/db generateでマイグレ生成 → commit →mainpush で GHA (deploy-staging.ymlのmigrateジョブ) が deploy 前に 自動適用 (追加系のみ自動。破壊的マイグレはガードがブロックし手動適用) - Secret 変更 →
gcloud secrets versions add ...→ Cloud Run を再 deploy (orgcloud run services update --update-secrets)
1. 通常のコード変更フロー
1-a. apps/web/** を変更
sh
# 編集 → commit
git add apps/web/...
git commit -m "feat(web): ..."
git push origin mainこれだけ。Netlify が GitHub 連携で main push を検知し、apps/web の build を起動 → production context で deploy。約 2-3 分。
確認:
sh
cd apps/web
netlify status # current site = liga-staging
netlify open # ブラウザで Netlify dashboard を開く
curl -i https://liga-staging.netlify.app/ # HTTP 200 確認1-b. apps/api/** を変更
sh
git add apps/api/...
git commit -m "feat(api): ..."
git push origin mainGitHub Actions の deploy-staging.yml が main push をトリガに起動 → Docker build → Artifact Registry へ push → Cloud Run service liga-api-staging を新 image で update。約 3-5 分。
確認:
sh
gh run watch --exit-status # 完了まで CLI で監視
gh run list -L 3 -w deploy-staging.yml
curl -i https://liga-api-staging-2cu6gfwvvq-an.a.run.app/healthz1-c. packages/* (db / shared) だけを変更
ここが 注意ポイント。
- Cloud Run 側: Docker build が
packages/*を含めるので、apps/api変更がなくても GHA で deploy できる。ただし GHA はapps/api/**パス変更でしかトリガしない設定には なってない (現状は main push 全部で動く) のでそのまま push で反映される - Netlify 側:
netlify.tomlのbase = "apps/web"を見て build content change を 判定するので、packages/*だけの push は "Canceled build due to no content change" で skip される
対策:
sh
# 手動で Netlify build を発火
cd apps/web
netlify deploy --build --prod詳細は 踏んだ落とし穴 ⑯。
1-d. infra/terraform/** を変更
自動では走らない。ローカルから手動 apply。
sh
cd infra/terraform
terraform plan -var-file=envs/staging.tfvars # 差分確認
terraform apply -var-file=envs/staging.tfvars -auto-approveState は GCS バケットに保存されてるので他の人が apply した後でも terraform plan で差分 確認できる。
2. DB スキーマ変更
packages/db/src/schema/*.ts を編集する変更は マイグレーション生成 + 適用 の 2 step。
Step 1 — マイグレーション生成 (ローカル / dev DB 立ち上げ済み前提)
sh
# dev の Postgres を起動 (docker-compose)
pnpm dev:db:up
# schema を編集
$EDITOR packages/db/src/schema/tournaments.ts
# 差分から SQL を自動生成
pnpm -F @liga/db generate
# → packages/db/drizzle/NNNN_<random>.sql が増える
# → packages/db/drizzle/meta/NNNN_snapshot.json も更新される
# dev DB に適用して動作確認
pnpm -F @liga/db migrate # DATABASE_URL は dev のローカル DB
pnpm -F @liga/db seed # 必要なら seed 再投入
# commit
git add packages/db/
git commit -m "feat(db): add foo column to tournaments"drizzle-kit push は使わない
push は schema を直接 DB に当てるだけでマイグレファイルを残さない。dev で個人実験するとき 以外は禁止。必ず generate → migrate で履歴に残す。
Step 2 — staging DB へ適用 (通常は自動 / 例外は手動)
通常は自動。 main push で deploy-staging.yml の migrate ジョブが deploy-api の 前段 (needs: migrate) として走り、未適用マイグレを Cloud SQL Auth Proxy 経由で適用する。
- 追加系 (ADD COLUMN 等) は自動適用 される
- 破壊的マイグレ (DROP COLUMN/TABLE・型変更・TRUNCATE) はガードがブロック して
migrateジョブを fail させる (deploy もneedsで止まる)。scripts/ci/check-destructive-migrations.shが実 DB の未適用 (pending) 集合をdrizzle.__drizzle_migrationsと_journal.jsonから 特定して検査する - 破壊的を承知で自動適用したいときは
workflow_dispatchのallow_destructive=trueで実行する
ガードにブロックされた破壊的マイグレや prod は、下記の手動手順で適用する。
sh
# 1) Cloud SQL Auth Proxy をローカルで起動 (背景プロセス)
cloud-sql-proxy \
--port 5433 \
project-0fccf552-fc68-4756-9c1:asia-northeast1:liga-staging &
# 2) staging の DATABASE_URL を Secret から取得して Proxy 越しに繋ぐ形に書き換える
# (Secret は postgres://liga:<pass>@/liga?host=/cloudsql/<conn> の Unix socket 形式)
DB_PASS=$(gcloud secrets versions access latest --secret=DATABASE_URL_staging | \
sed -E 's|.*://liga:([^@]+)@.*|\1|')
export DATABASE_URL="postgres://liga:${DB_PASS}@127.0.0.1:5433/liga"
# 3) migrate
pnpm -F @liga/db migrate
# 4) Proxy を止める
kill %1ローカル dev DB と Proxy のポート衝突
ローカル開発の Postgres も 5433 を使う (pnpm dev:db:up)。手動で Proxy を立てるときは ローカル DB を止めるか、Proxy を別ポート (例 --port 5434) にして DATABASE_URL も合わせる。
Step 3 — アプリ側 deploy (schema 変更とアプリ変更が両方ある場合)
- 追加系は
migrateジョブが deploy より先に自動適用するので、順序は自動で守られる (新カラムが無い状態で新アプリが SELECT して落ちる事故を防ぐ) - 破壊的マイグレ (カラム削除等) は expand-contract で:
- アプリ側で先にカラム参照を消して deploy
- 確認後にカラム削除マイグレを書き、ガードのブロックを承知で手動適用 (or
allow_destructive=true)
staging migrate の GHA 化 (実装済み)
deploy-staging.yml に migrate ジョブを実装済み (deploy-api は needs: migrate)。要点:
- WIF で GCP 認証 → Secret Manager
DATABASE_URL_stagingから接続情報取得 → Cloud SQL Auth Proxy (127.0.0.1:5433) 経由でpnpm db:migrate - proxy バイナリは公式 GCS の固定 SHA256 で検証。DB パスワードは
::add-mask::でログマスク - 破壊的マイグレは
scripts/ci/check-destructive-migrations.shがブロック (上記) - IAM は
github-actions-${env}SA のsecretAccessor(DATABASE_URL_${env}) + project-levelcloudsql.clientを使用 (terraform 既存)
prod は自動 deploy パイプライン自体が無いため、prod の migrate は引き続き手動。
3. Secret 値の変更
*_staging Secret を上書き
sh
# 例: SENDGRID_API_KEY_staging を新しい値に
echo -n "SG.real-key-here" | \
gcloud secrets versions add SENDGRID_API_KEY_staging --data-file=-新しい version が latest になる。
Cloud Run に拾わせる
Cloud Run の secret_key_ref { version = "latest" } は service revision 作成時点 で latest を解決する。なので Secret を更新しても自動では拾わない。次の deploy で拾う、もしくは:
sh
gcloud run services update liga-api-staging \
--region asia-northeast1 \
--update-secrets SENDGRID_API_KEY=SENDGRID_API_KEY_staging:latest(同じ参照を --update-secrets で再指定するだけで新 revision が作られて latest を解決)
Netlify に拾わせる
sh
cd apps/web
netlify env:set FOO "bar" --context production --force
netlify deploy --build --prodenv 反映には 再 build が必要 (Functions のビルド成果物に env がスナップショットされるため)。
詳細 踏んだ落とし穴 ⑭。
4. 巻き戻し (rollback)
Netlify (apps/web)
UI から: Deploys → 旧 deploy を選んで "Publish deploy"。
CLI から or git で:
sh
git revert <bad-sha>
git push origin main # auto deployCloud Run (apps/api)
sh
# 直近の revision 一覧
gcloud run revisions list --service liga-api-staging --region asia-northeast1
# 1 つ前の revision に traffic 100% を寄せる
gcloud run services update-traffic liga-api-staging \
--region asia-northeast1 \
--to-revisions liga-api-staging-00007-abc=100Cloud Run は image 自体を AR に残してるので、git revert + push でも結果的に元に戻る (古い image を build し直す形)。緊急時は update-traffic のほうが速い。
DB マイグレーション
drizzle-kit は rollback 用 SQL を自動生成しない。下のいずれか:
- 破壊的でないマイグレ (
ADD COLUMN ... NULL) なら、アプリ側のgit revertで済む (カラムは残るが使われない、後でDROP COLUMNマイグレを書く) - 破壊的なマイグレ (
DROP COLUMN,ALTER ... TYPE) は 手書きで rollback SQL を別途用意 してから本番適用する
5. CI が通らないと merge / deploy できないようにする (将来の改善)
current: main 直 push 運用。事故を起こしやすい。
将来案:
- main を branch protection で 「PR + CI green + 1 review approval」 必須に
ci.yml(typecheck + build) を required check に- staging deploy は main merge をトリガに継続
そうすると流れは:
ローカル編集 → PR → CI 緑 → review → main merge → 自動 staging deploy6. dev サーバの立て方 (ローカル)
参考までに。
sh
# 依存
pnpm i
# dev DB (docker-compose で Postgres 17)
pnpm dev:db:up
# migrate + seed
pnpm -F @liga/db migrate
pnpm -F @liga/db seed
# web (Next.js) と api (Hono) を並列起動
pnpm dev
# web : http://localhost:3100
# api : http://localhost:30017. ローカルでメールを確認する (dry-run)
メール送信 (@liga/mailer、SendGrid SDK) は MAIL_DRY_RUN=true のとき実送信せず、 宛先 / 件名 / 本文 / 本文中のリンクを API のコンソールにダンプする。SendGrid の 設定 (API キー・Sender 認証) なしでメール周りを確認できる。
認証メール (確認 / パスワードリセット / メール変更) は apps/api が送る (認証は API に 集約済み) ので、ダンプは API ログに出る。
sh
# apps/api/.env.local に 1 行
MAIL_DRY_RUN=true
# → API を再起動 (tsx watch は env-file 変更を拾わないので必須)sh
# 送信内容を見る (API を pnpm dev のログに出している前提)
grep -A8 "mailer DRY-RUN" <api ログ>
# リアルタイム追尾 + メールだけ抽出
tail -f <api ログ> | grep --line-buffered -A8 "mailer DRY-RUN"出力例 (パスワードリセット):
[mailer DRY-RUN] to=... subject=[LiGA] パスワードリセットのご案内
...
http://localhost:3100/api/auth/reset-password/<token>?callbackURL=%2Freset-password%2Fconfirmリンクは http://localhost:3100/... を指すので、コピーしてブラウザで開けばメール ゲートのフロー (リセット確定 / メール確認) まで完結確認できる。
dev / staging / prod の方針
- dev:
apps/api/.env.localにMAIL_DRY_RUN=true - staging:
cloud_run.tfでvar.env == "staging"のときMAIL_DRY_RUN=trueを投入 (実送信しない。ダンプは Cloud Run ログ) - prod:
MAIL_DRY_RUNを入れない = 実送信。別途 SendGrid の API キー + Sender 認証 (Single Sender か Domain Authentication) が必要
MAIL_DRY_RUN を入れず SENDGRID_API_KEY も未設定だと、mailer は 黙って no-op (内容も出ない) になる点に注意。ローカル確認時は必ず MAIL_DRY_RUN=true を入れる。
関連
- Terraform 設計 / 運用 — IaC の構造
- staging 立ち上げログ — 初回構築の手順
- 踏んだ落とし穴 — 既知の罠