記事一覧に戻る
ステージング/本番環境を1つのワークフローで管理するデプロイ戦略

ステージング/本番環境を1つのワークフローで管理するデプロイ戦略

GitHub Actionsでmainブランチ→ステージング、タグ→本番のデプロイを1ワークフローで実現。SOPSでの環境別シークレット管理、Turborepoでの並列デプロイも解説。

「本番直デプロイ」で済ませがちですが、やっぱり本番前に確認したい場面は多い。

mainブランチへのpushでステージング、リリースタグで本番、という構成にしています。1つのGitHub Actionsワークフローで両方を管理していて、Turborepoのおかげで各アプリのデプロイも効率的に回っています。

構成の概要

使っているサービスと環境:

コンポーネントサービス環境別の分離
APICloud Runmyapp-stg-api / myapp-prod-api
WebCloudflare Pagesmyapp-stg-web / myapp-prod-web
WorkersCloud 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にマージしたら自動でステージングに上がる安心感
  • 本番はタグを打つだけでデプロイできる手軽さ
  • 環境ごとにリソースが分離されているので影響が限定的

ちゃんと本番前確認できる環境があると精神的に楽です。