Skip to content

staging 立ち上げログ

project-0fccf552-fc68-4756-9c1 プロジェクトに staging 環境を構築した記録と、 未完了タスクの引き継ぎメモ。

プロジェクト情報

項目
GCP Project IDproject-0fccf552-fc68-4756-9c1
GCP Project Number4967862325
Regionasia-northeast1
Billing有効 (billing account 010F01-089F36-47D33D)
Netlify Siteliga-staging (https://liga-staging.netlify.app)
Cloud Run URLhttps://liga-api-staging-2cu6gfwvvq-an.a.run.app
Cloud SQL Connproject-0fccf552-fc68-4756-9c1:asia-northeast1:liga-staging
Cloud SQL Public IP35.189.129.15
Artifact Registryasia-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 login
  • gcloud 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.tfvarsproject_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 で修正済み):

  1. db-f1-micro が ENTERPRISE_PLUS edition で拒否 → cloud_sql.tfsettings.edition = "ENTERPRISE" を明示
  2. Cloud Run の env block に予約名 PORT を含めて 400 → cloud_run.tfPORT env を削除 (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=-
done

DATABASE_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 --force

context のハマりどころ

最初 --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.tfauthorized_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 つ問題があり修正:

  1. COPY ... 2>/dev/null || true は Dockerfile では非対応 (shell リダイレクトは RUN だけ)。/2>/dev/null というファイルを探しに行って fail
  2. pnpm -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 --prod

curl -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-micromax_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/loginNetlify (Next.js)
APIhttps://liga-api-staging-2cu6gfwvvq-an.a.run.app/Cloud Run (Hono)
DB35.189.129.15:5432 (public IP, sslmode=no-verify)Cloud SQL (PostgreSQL 17)

残タスク (リリースまで)

残タスク内容
Stripe Webhook 登録完了 (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.tsapp.route("/webhooks/stripe", paymentWebhookApp))。受信する event は:

  • payment_intent.succeeded → registration.confirmed + ポイント消費
  • payment_intent.payment_failed → payments.failed
  • それ以外は 200 + event_type_ignored skip

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 に変換される (updatedAtupdated_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 で取得可