【実務・中級編】大規模開発の味方!GitHub Actionsの「Concurrency Group」で排他制御を行い、デプロイ事故を未然に防ぐ – バージョン管理・CI/CD活用バイブル

大規模開発の戦場:GitHub Actionsの`concurrency`でデプロイ事故を撲滅する排他制御の極意

テックリードの皆さん、日々のデプロイメンバリングご苦労様です。
一日に何十回もPRがマージされ、mainブランチへのPushが止まらない大規模開発の現場。あなたもこんな悪夢を見たことがないだろうか。

「Feature Aのデプロイが走っている最中に、緊急修正のFeature Bがマージされ、古いFeature Aの成果物が後から本番環境に上書きデプロイされて本番が死んだ」

……冷や汗が出る光景だ。CI/CDパイプラインは、正しく設定されていなければ「自動で高速にバグを本番に届ける装置」になってしまう。

今回は、この並行実行の競合(Race Condition)をエレガントに、かつ確実に屠るGitHub Actionsの秘技、`concurrency`(同時実行制御)の極限の活用法を伝授しよう。

—

なぜ従来のCI/CDはデプロイ競合に弱いのか?

GitHub Actionsはデフォルトでは「並行実行(Concurrence)」を許容する。同じブランチ、あるいは同じPRに対して短時間で何度もPush(あるいはイベント発火)が行われると、それぞれ独立したジョブが同時に走り出す。

これがテストの実行だけであれば「リソースの無駄遣い」で済むが、デプロイメントが絡んだ瞬間に致命傷になる。

  • 上書き問題 (Last-write-winsの罠): Job A(古いコミット)とJob B(新しいコミット)が同時にビルドを開始し、運悪くJob Aの方が遅れて完了した場合、新しい本番環境が古いコードで上書きされる。
  • リソースの枯渇とAPI制限: クラウドプロバイダ(AWS/GCPなど)へのデプロイ時にAPIの競合が起き、デプロイが中途半端な状態で失敗する。

これを防ぐために、かつてはステートフルなロック機構を外部ストレージに自前で実装したり、複雑なシェルスクリプトで排他制御を頑張ったりしていた。だが、もうそんな無駄な努力は必要ない。GitHub Actionsには`concurrency`という強力なネイティブ機能が備わっているのだから。

—

現場で即効性を発揮する `concurrency` のYAMLベストプラクティス

百聞は一見にしかず。まずは、実務で絶対に導入すべきプロダクション・グレードのYAML設定を見てほしい。

パターン1:PRの連続Push時は「古い実行を即座にキャンセル」する

開発中のPRにおいて、タイポ修正などで数分のうちに何度もPushを繰り返すことはよくある。この場合、古いコミットのCI/CDを走らせ続けるのは電力的にもサーバーリソース的にも無駄だ。新しいPushが来たら、古いジョブを容赦なくキャンセル(`cancel-in-progress: true`)させよう。

name: Continuous Integration & Review

on:
pull_request:
branches: [ “main” ]

🔑 ここが肝:PR単位でグループ化し、後続が来たら前を殺す
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
test-and-lint:
runs-on: ubuntu-latest
steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Set up Node.js

uses: actions/setup-node@v4
with:
node-version: ’20’
cache: ‘npm’

  • name: Install dependencies

run: npm ci

  • name: Run heavy test suite

run: npm test

この設定のキモ:

`github.event.pull_request.number || github.ref` という三項演算子を使っている点に注目してほしい。これにより、PRからのトリガーであれば「PR番号」をグループキーにし、マージ後の通常プッシュであれば「ブランチ名(`github.ref`)」をグループキーにできる。柔軟かつ堅牢なキー設計だ。

—

パターン2:本番デプロイは「キャンセルせず、順番待ち(Queue)させる」

ここが最も重要なポイントだ。本番環境(Production)へのデプロイメントにおいては、古いジョブを勝手にキャンセルしてはならない。
「古いデプロイをキャンセルし、新しいデプロイをキューに積んで順番に実行する(`cancel-in-progress: false`)」のが、デプロイ事故を防ぐ鉄則である。

name: Production Deployment

on:
push:
branches: [ “main” ]

🔑 本番環境はキューイング(直列化)が絶対条件
concurrency:
group: production-deployment
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:

  • name: Checkout code

uses: actions/checkout@v4

  • name: Authenticate to Cloud Provider

uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}

  • name: Execute Database Migration & Deploy

run: |
echo “安全な順次デプロイを開始します…”
./scripts/deploy-to-prod.sh

なぜ `cancel-in-progress: false` なのか?

もしこれを `true` にしてしまうと、Aのデプロイ途中にBがマージされた場合、Aのデプロイが中途半端な状態(DBマイグレーションだけ終わっていてアプリコードが古い状態など)で強制終了され、システムが致命的な不整合を起こす。
キューイング(FIFO:先入れ先出し)にすることで、Aが完全に終わった後に、安全にBのデプロイが後続として走り出す。これぞプロのインフラ設計だ。

—

チーム開発を加速させる「設定の共有化ルール」とハック

`concurrency` は非常に強力だが、チームメンバー全員がその挙動を理解し、適切にYAMLに記述し忘れないようにするのはマネジメントのコストがかかる。ここで、テックリードとして組織に組み込むべきプラクティスを共有しよう。

1. 再利用可能なワークフロー(Reusable Workflows)への組み込み

デプロイのロジック自体をReusable Workflowとして切り出し、その内部に `concurrency` をハードコディングするのが最も美しい。開発者は呼び出すだけで、強制的に排他制御の恩恵を受けられる。

.github/workflows/reusable-deploy.yml (プラットフォームチームが管理)
name: Reusable Deploy Pipeline

on:
workflow_call:
inputs:
environment:
type: string
required: true

💡 共通ワークフロー側でconcurrencyを強制する
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false

jobs:
deploy:
runs-on: ubuntu-latest
steps:

  • run: echo “Deploying to ${{ inputs.environment }}…”

2. GitHub Environmentsの「Deployment protection rules」との併用

`concurrency` はあくまで「同一グループ内の実行制御」だが、GitHubの Environments 機能と組み合わせることで、さらに堅牢性が増す。

  • `production` 環境に対してレビュアーの承認(Required reviewers)を必須にする。
  • `concurrency` でキューに積まれたデプロイメントが、順番に人間の目で承認されながら安全に適用されていく。このフローが完成した時、あなたのチームのデプロイ品質は業界トップクラスになる。

—

生産性を落とさないための「キーボードショートカット」&神ツール

最後に、CI/CDを日常的にハックするエンジニアへ、開発スピードを落とさないためのオマケだ。

  • GitHub CLI (`gh`) でのワークフロー強制キャンセル:

キューが詰まったり、デプロイがスタックしたときにブラウザをポチポチするのは時間の無駄だ。ターミナルから一発で叩けるようにしておこう。

# 実行中のワークフローをインタラクティブに確認・キャンセル
gh run cancel $(gh run list –status in_progress –limit 5 –json databaseId -q ‘.[].databaseId’)

  • VS Code Extension: `GitHub Actions` (by GitHub)

エディタ上でYAMLのシンタックスチェックだけでなく、ワークフローの実行ステータスやログの確認まで完結させる。コンテキストスイッチをゼロにすることが、個人の生産性を極限まで高めるカギだ。

—

まとめ

`concurrency` は、地味ながら大規模開発のデプロイ事故を防ぐ最強の盾である。

1. PRのCIは `cancel-in-progress: true` で無駄な実行を殺せ。
2. 本番デプロイは `cancel-in-progress: false` で絶対に順序を守れ。
3. 共通化とEnvironmentsを組み合わせて、チーム全体のミスを構造的に防げ。

明日からのパイプラインを見直し、安全で高速なデプロイメントを手に入れろ。あなたのチームのコードが、今日も安全に本番へ届くことを祈っている。

タイトルとURLをコピーしました