ステージング/本番環境を1つのワークフローで管理するデプロイ戦略
GitHub Actionsでmainブランチ→ステージング、タグ→本番のデプロイを1ワークフローで実現。SOPSでの環境別シークレット管理、Turborepoでの並列デプロイも解説。
「本番直デプロイ」で済ませがちですが、やっぱり本番前に確認したい場面は多い。
mainブランチへのpushでステージング、リリースタグで本番、という構成にしています。1つのGitHub Actionsワークフローで両方を管理していて、Turborepoのおかげで各アプリのデプロイも効率的に回っています。
構成の概要
使っているサービスと環境:
| コンポーネント | サービス | 環境別の分離 |
|---|---|---|
| API | Cloud Run | myapp-stg-api / myapp-prod-api |
| Web | Cloudflare Pages | myapp-stg-web / myapp-prod-web |
| Workers | Cloud Run Jobs | 同様 |
ステージングと本番で別リソースなので、完全に独立して動きます。
トリガーの設計
# .github/workflows/deploy.yml
on:
push:
branches:
- main
tags:
- 'release*'
mainへのpush → ステージングデプロイrelease*タグ → 本番デプロイ
本番デプロイは手動で git tag release-20260102 && git push origin release-20260102 と打つ形式。誤デプロイを防ぐために意図的にこうしています。
環境判定
ワークフローの最初で環境を判定:
- name: Determine environment
id: env
run: |
if [[ "${{ github.ref }}" == refs/tags/release* ]]; then
echo "deploy_env=prod" >> $GITHUB_OUTPUT
echo "secrets_file=terraform/secrets/prod.enc.yaml" >> $GITHUB_OUTPUT
else
echo "deploy_env=stg" >> $GITHUB_OUTPUT
echo "secrets_file=terraform/secrets/stg.enc.yaml" >> $GITHUB_OUTPUT
fi
この deploy_env が後続の全ステップで参照されます。
シークレット管理
環境別のシークレットはSOPSで暗号化してリポジトリにコミットしています。
secrets/
├── stg.enc.yaml # ステージング用(KMSで暗号化)
└── prod.enc.yaml # 本番用(KMSで暗号化)
GitHub Actions内でクラウド認証後にSOPSで復号する形です。KMSの鍵はWorkload Identity経由でアクセスするので、シークレットがGitHub側に保存されることはありません。
フロントエンドの環境切り替え
Viteの --mode オプションを使って、環境別の .env ファイルを読み込みます:
# package.json
"build": "vite build --mode ${DEPLOY_ENV:-development}"
環境変数ファイル:
apps/web/
├── .env.development # ローカル開発
├── .env.stg # ステージング
└── .env.prod # 本番
# .env.stg
VITE_API_BASE_URL=https://api.stg.example.com
VITE_APP_ENV=stg
ビルド時に DEPLOY_ENV=stg pnpm build とすれば、.env.stg が読み込まれます。
バックエンドのイメージ管理
Cloud Run用のDockerイメージは、環境別のArtifact Registryにpushします。イメージタグにはGitのコミットハッシュを使用。同じコミットなら同じイメージ、という対応が取れるので、「このバージョンのイメージどれだっけ」が分かりやすい。
Turborepoでの並列デプロイ
各アプリのデプロイはTurborepoのタスクとして定義:
// turbo.json
{
"tasks": {
"@myapp/api#deploy": {
"dependsOn": ["@myapp/api#docker-build"],
"cache": false
},
"@myapp/web#deploy": {
"dependsOn": ["build"],
"cache": false
}
}
}
ワークフローではこれらを一度に呼び出す:
- name: Deploy
run: pnpm turbo run @myapp/api#deploy @myapp/web#deploy
Turborepoが依存関係を解決して、APIのdocker-build → deploy、Webのdeploy を並列で実行してくれる。
静的サイトの統合
Tech Blog(Astro)はWebのデプロイ時に統合されます。Webアプリの dist/ の中に blog/ ディレクトリをコピーして、/blog/ パスでアクセス可能にする形です。
同時デプロイの防止
複数のpushが連続した場合、古いデプロイがキャンセルされるように設定:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: true
同じブランチ/タグへの新しいpushがあれば、進行中のジョブは中断される。リソースの無駄遣い防止。
失敗通知
デプロイ失敗時はSentryにイベントを送信:
- name: Notify deploy failure to Sentry
if: failure()
run: |
npx @sentry/cli send-event \
--message "Deploy failed: ${{ steps.env.outputs.deploy_env }}" \
--level error \
--tag environment:${{ steps.env.outputs.deploy_env }}
Sentryのアラート機能でメール通知が来るので、失敗を見逃すことがありません。
ローカルでの確認
デプロイスクリプトはローカルでも実行可能にしています。クラウド認証済みの状態で実行すれば動くので、CIが壊れているときの緊急対応に使えます。
運用してみて
この構成で運用してみると:
- mainにマージしたら自動でステージングに上がる安心感
- 本番はタグを打つだけでデプロイできる手軽さ
- 環境ごとにリソースが分離されているので影響が限定的
ちゃんと本番前確認できる環境があると精神的に楽です。