Appearance
staging 立ち上げログ
project-0fccf552-fc68-4756-9c1 プロジェクトに staging 環境を構築した記録と、 未完了タスクの引き継ぎメモ。
プロジェクト情報
| 項目 | 値 |
|---|---|
| GCP Project ID | project-0fccf552-fc68-4756-9c1 |
| GCP Project Number | 4967862325 |
| Region | asia-northeast1 |
| Billing | 有効 (billing account 010F01-089F36-47D33D) |
| Netlify Site | liga-staging (https://liga-staging.netlify.app) |
| Cloud Run URL | https://liga-api-staging-2cu6gfwvvq-an.a.run.app |
| Cloud SQL Conn | project-0fccf552-fc68-4756-9c1:asia-northeast1:liga-staging |
| Cloud SQL Public IP | 35.189.129.15 |
| Artifact Registry | asia-northeast1-docker.pkg.dev/project-0fccf552-fc68-4756-9c1/liga-images |
完了したフェーズ
Phase 0 — 事前準備
- CLI インストール:
gcloud/terraform/cloud-sql-proxy/netlify gcloud auth login+gcloud auth application-default logingcloud config set project project-0fccf552-fc68-4756-9c1- State 用 GCS バケット作成 (versioning ON + labels):sh
gcloud storage buckets create gs://project-0fccf552-fc68-4756-9c1-tf-state \ --location=asia-northeast1 \ --uniform-bucket-level-access gcloud storage buckets update gs://project-0fccf552-fc68-4756-9c1-tf-state \ --update-labels=app=liga,managed-by=terraform,component=tfstate gsutil versioning set on gs://project-0fccf552-fc68-4756-9c1-tf-state
Phase 1 — Terraform apply
infra/terraform/envs/staging.tfvars の project_id を実値に置換した上で:
sh
cd infra/terraform
terraform init \
-backend-config="bucket=project-0fccf552-fc68-4756-9c1-tf-state" \
-backend-config="prefix=staging"
terraform apply -var-file=envs/staging.tfvars途中で当たったエラー 2 件 (commit bf21ffb で修正済み):
db-f1-microが ENTERPRISE_PLUS edition で拒否 →cloud_sql.tfにsettings.edition = "ENTERPRISE"を明示- Cloud Run の
envblock に予約名PORTを含めて 400 →cloud_run.tfのPORTenv を削除 (ports.container_portで自動セット)
最終的に 71 リソース作成完了。
Phase 2 — Secret 値投入
sh
PROJECT_ID=project-0fccf552-fc68-4756-9c1
ENV=staging
gcloud config set project $PROJECT_ID
# 乱数生成系
printf "%s" "$(openssl rand -base64 32)" | gcloud secrets versions add BETTER_AUTH_SECRET_${ENV} --data-file=-
printf "%s" "$(openssl rand -hex 16)" | gcloud secrets versions add CRON_SECRET_${ENV} --data-file=-
# Stripe (apps/api/.env.local の test key を流用)
STRIPE_SK=$(grep '^STRIPE_SECRET_KEY=' apps/api/.env.local | cut -d= -f2-)
STRIPE_PK=$(grep '^STRIPE_PUBLISHABLE_KEY=' apps/api/.env.local | cut -d= -f2-)
printf "%s" "$STRIPE_SK" | gcloud secrets versions add STRIPE_SECRET_KEY_${ENV} --data-file=-
printf "%s" "$STRIPE_PK" | gcloud secrets versions add STRIPE_PUBLISHABLE_KEY_${ENV} --data-file=-
printf "TODO" | gcloud secrets versions add STRIPE_WEBHOOK_SECRET_${ENV} --data-file=- # 後で書き換え
# その他 (現状未契約サービスは TODO)
for K in SENDGRID_API_KEY SLACK_BOT_TOKEN GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY; do
printf "TODO" | gcloud secrets versions add ${K}_${ENV} --data-file=-
doneDATABASE_URL は TF が自動投入済み (構成要素が TF 内にあるため例外的)。
Phase 3 — DB マイグレ + seed
sh
# 別ターミナルで Cloud SQL Auth Proxy
INSTANCE=project-0fccf552-fc68-4756-9c1:asia-northeast1:liga-staging
cloud-sql-proxy $INSTANCE
# 元ターミナル: proxy 経由で migrate + seed
DB_URL_RAW=$(gcloud secrets versions access latest --secret=DATABASE_URL_staging)
export DATABASE_URL=$(echo "$DB_URL_RAW" | sed -E 's|@/([^?]+)\?host=/cloudsql/[^"]+|@127.0.0.1:5432/\1|')
cd /Users/tanaka/projects/tmp/liga-mono
pnpm -F @liga/db migrate
pnpm -F @liga/db seed確認: tournaments=9, users=0
Phase 4 — GitHub Secrets / Variables
gh api -X PUT repos/astyleinc/liga-mono/environments/staging で staging Environment を作成 (gh secret set -e は環境を自動作成しないため事前必須)。
その後、Terraform output の値を投入:
sh
WIF=$(terraform output -raw workload_identity_provider)
SA=$(terraform output -raw github_actions_service_account_email)
gh secret set GCP_PROJECT_ID -e staging -b "project-0fccf552-fc68-4756-9c1"
gh secret set GCP_REGION -e staging -b "asia-northeast1"
gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER -e staging -b "$WIF"
gh secret set GCP_SERVICE_ACCOUNT -e staging -b "$SA"
gh secret set CLOUD_RUN_SERVICE -e staging -b "liga-api-staging"
gh secret set AR_REPOSITORY -e staging -b "liga-images"
gh variable set ENABLE_STAGING_DEPLOY -e staging -b "true"Phase 5 — Netlify env vars (production context に明示投入)
liga-staging site の production context に投入:
sh
cd apps/web
# Cloud SQL は Netlify (AWS Lambda) から Unix socket では繋がらないので
# public IP 形式に書き換える (sslmode=require)
DB_PASS=$(gcloud secrets versions access latest --secret=DATABASE_URL_staging | \
sed -E 's|.*://liga:([^@]+)@.*|\1|')
DB_PUBLIC=$(cd ../../infra/terraform && terraform output -raw cloud_sql_public_ip)
DATABASE_URL_PUBLIC="postgres://liga:${DB_PASS}@${DB_PUBLIC}:5432/liga?sslmode=require"
netlify env:set DATABASE_URL "$DATABASE_URL_PUBLIC" --context production --force
CLOUD_RUN_URL=$(cd ../../infra/terraform && terraform output -raw cloud_run_url)
netlify env:set NEXT_PUBLIC_API_URL "$CLOUD_RUN_URL" --context production --force
netlify env:set BETTER_AUTH_SECRET \
"$(gcloud secrets versions access latest --secret=BETTER_AUTH_SECRET_staging)" \
--context production --force
netlify env:set BETTER_AUTH_URL "https://liga-staging.netlify.app" --context production --force
netlify env:set STRIPE_SECRET_KEY \
"$(gcloud secrets versions access latest --secret=STRIPE_SECRET_KEY_staging)" \
--context production --force
netlify env:set NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY \
"$(gcloud secrets versions access latest --secret=STRIPE_PUBLISHABLE_KEY_staging)" \
--context production --forcecontext のハマりどころ
最初 --context branch-deploy で投入してしまい、main ブランチからの deploy (production context) に値が反映されない事故が発生。netlify env:get --context production を見ると placeholder のままだったために getaddrinfo ENOTFOUND placeholder.invalid で web が 500。
修正: --context production を明示 して再投入。Netlify は main push を production context として扱うため。liga-staging site は staging 専用なので production context = staging 環境という慣習にも合う。
DATABASE_URL のフォーマット
Secret Manager に入れた DATABASE_URL は Cloud SQL の Unix socket 形式 (postgres://liga:pw@/liga?host=/cloudsql/<conn>)。これは Cloud Run でしか動かず、 Netlify (AWS Lambda) では getaddrinfo ENOTFOUND /cloudsql/... で死ぬ。
Netlify 用に Cloud SQL public IP 形式 (postgres://liga:pw@35.x.x.x:5432/liga?sslmode=require) に書き換えてから投入する。これに伴い:
cloud_sql.tfにauthorized_networks { value = "0.0.0.0/0" }をvar.env == "staging"の時だけ動的追加 (staging 専用、prod は別途 Private IP + VPC connector へ)sslmode=requireで暗号化を強制
Phase 6 — Cloud Run へ初回 deploy
GitHub Actions deploy-staging.yml が main push で自動キック。main ブランチが ENABLE_STAGING_DEPLOY ゲートで skip されていたので、対処:
- 当初
if: ${{ vars.ENABLE_STAGING_DEPLOY == 'true' }}を Environment level の Variable 参照で書いていたが、job 先頭のif:は environment が bind される前に 評価されるため Environment variable は読めない → 常に skip if:を撤去して main push or workflow_dispatch で常時動く設計に変更 (一時停止は PR をマージしないことで対応)
Dockerfile も 2 つ問題があり修正:
COPY ... 2>/dev/null || trueは Dockerfile では非対応 (shell リダイレクトは RUN だけ)。/2>/dev/nullというファイルを探しに行って failpnpm -F @liga/db -F @liga/shared buildは db/shared に build script が無くて ERR_PNPM_RECURSIVE_RUN_NO_SCRIPT で fail
→ 4-stage を 1-stage に簡素化、ランタイムは tsx で TS workspace import を 透過的に解決。CMD は apps/api を WORKDIR にして ./node_modules/.bin/tsx src/index.ts (pnpm は root の node_modules/.bin にバイナリを置かないため)。
Phase 7 — Netlify 再 deploy + SSL 設定 (staging 完成)
env 更新後、Netlify は再 build しないと反映されない (Functions のビルド成果物に env がスナップショットされる)。netlify deploy --build --prod でトリガー。
再 deploy 後の動作で UNABLE_TO_VERIFY_LEAF_SIGNATURE エラー発生。 Cloud SQL の Google CA cert を Node が信頼してないため。
修正: DATABASE_URL の ?sslmode=require を ?sslmode=no-verify に変更。
- SSL 通信 (encrypted) は維持
- Cert 検証だけスキップ
- 中間者攻撃には弱くなるが staging では妥協 (prod は CA cert を pg に渡す or Private IP に切替)
sh
DB_PASS=$(gcloud secrets versions access latest --secret=DATABASE_URL_staging | \
sed -E 's|.*://liga:([^@]+)@.*|\1|')
DB_PUBLIC=$(cd infra/terraform && terraform output -raw cloud_sql_public_ip)
DATABASE_URL_PUBLIC="postgres://liga:${DB_PASS}@${DB_PUBLIC}:5432/liga?sslmode=no-verify"
cd apps/web
netlify env:set DATABASE_URL "$DATABASE_URL_PUBLIC" --context production --force
netlify deploy --build --prodcurl -i https://liga-staging.netlify.app/ で HTTP/2 200 が返って完成 🎉
Phase 7.5 — Cloud SQL connection pool 枯渇修正
完成直後に /tournaments 等の RSC ページが「Server Components render エラー」を吐く問題が 発覚。Netlify Function ログを見ると:
error: remaining connection slots are reserved for roles with privileges of the
"pg_use_reserved_connections" role (code 53300)Cloud SQL db-f1-micro の max_connections = 25 に対し、pg.Pool({ max: 10 }) × 同時 warm container 数 で枯渇していた。packages/db/src/client.ts の Pool を Lambda 系前提に縮めて解消:
ts
new Pool({
connectionString: url,
max: 1,
idleTimeoutMillis: 10_000,
connectionTimeoutMillis: 5_000,
});詳細は 踏んだ落とし穴 ⑮。
なお packages/db だけの変更だと Netlify が base = apps/web の content change として 検知せず build を skip するので、netlify deploy --build --prod を手動で trigger する必要があった (詳細 ⑯)。
完成構成
| エンドポイント | URL | 役割 |
|---|---|---|
| 参加者フロント | https://liga-staging.netlify.app/ | Netlify (Next.js) |
| 管理画面 | https://liga-staging.netlify.app/manage/login | Netlify (Next.js) |
| API | https://liga-api-staging-2cu6gfwvvq-an.a.run.app/ | Cloud Run (Hono) |
| DB | 35.189.129.15:5432 (public IP, sslmode=no-verify) | Cloud SQL (PostgreSQL 17) |
残タスク (リリースまで)
| 残タスク | 内容 |
|---|---|
完了 (2026-05-14) — Stripe CLI で we_xxx を作成、whsec_… を Secret v3 に投入 → Cloud Run revision 00005-zc6 で反映。stripe trigger payment_intent.succeeded で 200 + 署名検証通過を確認 | |
完了 (2026-05-14) — j.myoga.work@gmail.com を staging で signup → Cloud SQL Auth Proxy 越しに UPDATE "user" SET role = 'admin' | |
| Cloud SQL の Private IP 化 | リリース直前に Private IP + Serverless VPC Access に切替 (Netlify は VPC Access 不可なので web からの直接アクセスを止めて全部 Cloud Run 経由にする要検討) |
| 独自ドメイン | 取得後に Netlify Custom domain + Google OAuth リダイレクト URI + Stripe Webhook URL を更新 |
| SendGrid 認証 | ドメイン取得後に Sender Authentication (SPF/DKIM) → API Key 取得 → Secret 上書き |
| Cloudflare R2 バケット | 画像アップロード機能を実装するタイミングで作成 + 認証情報を Secret に |
| GCP Free Trial 期限 | 90 日後 (2026-08-x) に有料アカウントへ手動 upgrade |
main push or gh workflow run deploy-staging.yml で GHA を起動。 Workload Identity Federation で Cloud Run に新 image を push。
Phase 8 — Stripe Webhook 登録 (完了 2026-05-14)
Stripe CLI で endpoint を作成 (Dashboard でも可):
sh
stripe login # test mode で連携 (初回のみ)
WHSEC=$(stripe webhook_endpoints create \
--url "https://liga-api-staging-2cu6gfwvvq-an.a.run.app/webhooks/stripe" \
--enabled-events payment_intent.succeeded \
--enabled-events payment_intent.payment_failed \
--description "LiGA staging" \
| grep -oE '"secret":\s*"[^"]+"' | sed -E 's/.*"([^"]+)"$/\1/')
echo "$WHSEC"Secret 投入 + Cloud Run に新 revision を作って latest を解決:
sh
echo -n "$WHSEC" | gcloud secrets versions add STRIPE_WEBHOOK_SECRET_staging --data-file=-
gcloud run services update liga-api-staging \
--region asia-northeast1 \
--update-secrets STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET_staging:latest動作確認:
sh
stripe trigger payment_intent.succeeded
# Cloud Run ログ:
# [stripe webhook] PI pi_xxx missing metadata; skipping ← 署名検証 ✓ / handler 正常実行 ✓
# --> POST /webhooks/stripe 200エンドポイントは /webhooks/stripe (apps/api/src/index.ts の app.route("/webhooks/stripe", paymentWebhookApp))。受信する event は:
payment_intent.succeeded→ registration.confirmed + ポイント消費payment_intent.payment_failed→ payments.failed- それ以外は 200 +
event_type_ignoredskip
Cloud Run URL の 2 形式
Cloud Run は service に 2 つの URL を払い出す:
- 旧形式:
https://liga-api-staging-2cu6gfwvvq-an.a.run.app - 新形式:
https://liga-api-staging-4967862325.asia-northeast1.run.app
どちらも同じ service を指す。Webhook には どちらか 1 個だけ 登録すれば OK。 登録後にカスタムドメインに切り替える場合は Stripe 側で endpoint を新 URL に更新する。
Phase 9 — 管理者アカウント作成 (完了 2026-05-14)
Better Auth + Drizzle adapter のテーブルは "user" (双引用必須、user は予約語)。 パスワードハッシュは別テーブル account に保存される (Better Auth が管理) ので、 Cloud SQL に直接 INSERT は危険。signup を UI 経由で済ませてから role を上げる。
sh
# 1) staging で signup (UI から)
# https://liga-staging.netlify.app/signup → メアド + パスワード or Google
# 2) Cloud SQL Auth Proxy 起動
cloud-sql-proxy --port 5435 \
project-0fccf552-fc68-4756-9c1:asia-northeast1:liga-staging &
# 3) DATABASE_URL_staging から password を取り出して psql
DB_URL=$(gcloud secrets versions access latest --secret=DATABASE_URL_staging)
DB_PASS=$(echo "$DB_URL" | sed -E 's|.*://liga:([^@]+)@.*|\1|')
export PGPASSWORD="$DB_PASS"
# 4) role を昇格 (カラムは snake_case)
psql -h 127.0.0.1 -p 5435 -U liga -d liga -c \
"UPDATE \"user\" SET role = 'admin', updated_at = now() \
WHERE email = '<your-email>' RETURNING id, email, role;"
# 5) Proxy 停止
kill %1反映タイミング
Better Auth の session には role の cached copy が含まれてる可能性があるため、 一度ログアウト → 再ログイン すると確実に新しい role が乗る。
column 名トラップ
schema.ts では camelCase で書いてるが、Drizzle の default mapping で DB 側は snake_case に変換される (updatedAt → updated_at)。psql で直接叩く ときは snake_case を使う。
Phase 8 — 動作確認
sh
curl https://liga-api-staging-2cu6gfwvvq-an.a.run.app/healthz
open https://staging--liga-staging.netlify.app引き継ぎメモ
- 全 Secret 12 個は
gcloud secrets list --filter="labels.env=staging"で確認できる TODOで埋めた SendGrid / Slack / Google OAuth / R2 は契約後に値を上書き- DB password は
gcloud secrets versions access latest --secret=DATABASE_URL_stagingでいつでも取り出せる - Cloud Run のログ:
gcloud run services logs read liga-api-staging --region=asia-northeast1 --limit=50 - Cloud SQL に接続:
gcloud sql connect liga-staging --user=liga - Terraform output の値はいつでも
cd infra/terraform && terraform output -jsonで取得可