大規模開発の味方!GitHub Actionsの「Concurrency Group」で排他制御を行い、デプロイ事故を未然に防ぐ
皆さん、こんにちは!開発チームの皆さん、毎日のコードプッシュ、お疲れ様です。
「あれ?さっきデプロイしたばかりなのに、もう新しいバージョンが…?」「いや、待てよ、もしかして前のデプロイがまだ終わってなかったかも…?」
こんな経験、ありませんか?特に大規模な開発チームになると、複数の開発者が同時に作業を進めるため、意図せずデプロイが競合してしまったり、最新のコードが反映されなかったりといった「デプロイ事故」が起こりやすくなります。せっかく自動化を進めているのに、これでは本末転倒ですよね。
でも、安心してください!GitHub Actionsには、そんな悩みを解決してくれる強力な機能、「Concurrency Group」があるんです。これを使いこなせば、デプロイの競合を防ぎ、より安全で安定したデプロイメントを実現できます。
今回は、この「Concurrency Group」の具体的な設定方法から、実務での運用ルール構築術まで、先輩エンジニアの視点から、皆さんの日々の作業が劇的に楽になるような、現場で震えるほど役立つ極限の知見を、魂を込めてお伝えします!
そもそも「Concurrency Group」って何? なぜ必要なの?
「Concurrency Group」とは、簡単に言うと、「特定のジョブ(ワークフローの実行単位)が同時に複数実行されないように制御する仕組み」のことです。
大規模開発では、以下のような状況でデプロイ競合が発生しやすくなります。
- 複数の開発者が同時に、同じブランチにコードをプッシュした場合:
それぞれのPushがトリガーとなってCI/CDパイプラインが走り、デプロイが複数回実行される可能性があります。
- CI/CDパイプラインの実行に時間がかかる場合:
前のデプロイが完了する前に次のデプロイが開始されてしまい、意図しない状態になることがあります。
このような状況で、もしデプロイ処理が冪等性(何度実行しても同じ結果になる性質)を持っていなかったり、状態管理が複雑だったりすると、以下のような悲惨な結果を招く可能性があります。
- 古いコードがデプロイされる
- デプロイが途中で失敗する
- 本番環境が不安定になる
「Concurrency Group」は、これらの問題を「排他制御」という形で解決します。これは、あるリソース(この場合はデプロイ処理)を同時に利用できるのは1つだけ、という考え方です。
例えば、`main` ブランチへのデプロイは、常に1つだけ実行されるように制限することで、デプロイ事故のリスクを大幅に減らすことができます。
GitHub Actionsでの「Concurrency Group」設定方法:YAMLで直感的に!
GitHub ActionsのワークフローはYAMLファイルで定義されます。ここで「Concurrency Group」を設定するのは驚くほど簡単です!
ワークフローのYAMLファイルのトップレベルに `concurrency` キーを追加するだけです。
.github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches:
- main # mainブランチへのpushでトリガー
jobs:
deploy:
runs-on: ubuntu-latest
# ここでconcurrencyを設定します!
concurrency:
# グループ名を定義します。同じグループ名を持つワークフローは排他制御されます。
# ここでは、特定のブランチ名とリポジトリ名を組み合わせることで、
# 各ブランチごとに独立した排他制御を実現しています。
# ${{ github.ref }} は現在のブランチ名(例: refs/heads/main)を表します。
# sanitize_ref を使うと、refから不要なプレフィックスを取り除いた、より分かりやすい名前になります。
group: deploy-${{ github.ref_name }}
# concurrency:
# group: deploy-${{ github.repository }}-${{ github.ref_name }} # リポジトリ名も含める場合
# group: deploy-main # 特定のブランチに限定する場合
# 競合発生時の挙動を指定します。
# cancel-in-progress: true にすると、新しいワークフローが開始された際に、
# 実行中の古いワークフローをキャンセルします。
# false (デフォルト) の場合は、新しいワークフローはキューに入れられ、
# 実行中のワークフローが終了するまで待ちます。
cancel-in-progress: true
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ’20’ # 使用するNode.jsのバージョンを指定
- name: Install dependencies
run: npm ci # npm install ではなく npm ci を使うことで、lockファイルに基づいた確実なインストールを行います。
- name: Build application
run: npm run build # アプリケーションのビルド
- name: Deploy to production
run: |
echo “Deploying to production…”
# ここに実際のデプロイコマンドを記述します。
# 例: aws s3 sync ./dist s3://my-production-bucket –delete
# 例: docker push your-docker-repo/your-app:latest && kubectl set image deployment/your-app your-app=your-docker-repo/your-app:latest
sleep 60 # デモ用に60秒待機
echo “Deployment finished successfully!”
各設定項目の解説
- `concurrency`: このブロックで並行実行の制御を設定します。
- `group`:
- 排他制御のグループ名を指定します。同じ `group` 名を持つワークフロー実行は、同時に1つしか実行されません。
- `${{ github.ref_name }}` は、ワークフローをトリガーしたブランチ名(例: `main`, `develop`)を表します。これにより、ブランチごとに独立した排他制御が実現できます。例えば、`main` ブランチへのデプロイは `deploy-main` グループ、`develop` ブランチへのデプロイは `deploy-develop` グループのように、それぞれ独立して管理できます。
- `${{ github.repository }}` を含めると、リポジトリ名もグループ名に含めることができます。これは、複数のリポジトリで同じワークフローを使用している場合に便利です。
- 特定のブランチのみを対象にする場合は、`deploy-main` のように直接ブランチ名を指定することも可能です。
- `cancel-in-progress: true`:
- これが非常に強力な設定です!
- 新しいワークフロー実行が開始され、かつ同じ `concurrency` グループに属するワークフローが既に実行中の場合、実行中の古いワークフローを自動的にキャンセルします。
- これにより、古いデプロイが完了する前に新しいデプロイが始まってしまう、という状況を防ぐことができます。常に最新のコードでのデプロイを保証しやすくなります。
- `false` (デフォルト) の場合、新しいワークフローはキューに入り、実行中のワークフローが終了するまで待機します。これは、デプロイの順番が重要な場合に有効ですが、最新性を優先する場合は `true` がおすすめです。
なぜ `${{ github.ref_name }}` を使うのがおすすめなのか?
`group: deploy-${{ github.ref_name }}` という設定は、以下のようなメリットがあります。
1. ブランチごとの独立した排他制御: `main` ブランチへのデプロイと `develop` ブランチへのデプロイは、それぞれ別のグループとして扱われます。つまり、`main` ブランチのデプロイが実行中でも、`develop` ブランチのデプロイは並行して実行される可能性があります(もちろん、`develop` ブランチ内での排他制御は維持されます)。これにより、開発の並行性を保ちつつ、デプロイの競合を防ぐことができます。
2. 柔軟性: 特定のブランチ(例: `main`)だけを厳密に排他制御したい場合は、`group: deploy-main` のように直接指定することも可能です。
実務での運用ルール構築術:CI/CDを「信頼できるもの」にするために
「Concurrency Group」を設定するだけで、デプロイ競合のリスクは減らせますが、さらに盤石な体制を築くためには、チーム内での運用ルールを明確にすることが重要です。
1. デプロイ対象ブランチの定義と `concurrency` グループの設計
- 本番デプロイ: `main` や `master` のような本番環境に直接デプロイされるブランチは、最も厳格な排他制御が必要です。`group: deploy-main` のように、専用のグループを設定しましょう。`cancel-in-progress: true` を設定し、常に最新のコードがデプロイされるようにするのがおすすめです。
- ステージング/プレビュー環境デプロイ: 開発中の機能確認や、本番に近い環境でのテストのために、`develop` や `staging` ブランチなどへのデプロイも考えられます。これらのブランチに対しても、`group: deploy-develop` のように、ブランチごとに独立した `concurrency` グループを設定しましょう。
- フィーチャーブランチのデプロイ: 各開発者が作成するフィーチャーブランチ(`feature/xxx`)へのデプロイは、通常、CIでのテスト実行に留まります。もしデプロイまで行う場合でも、`group: deploy-${{ github.ref_name }}` であれば、各フィーチャーブランチごとに排他制御されるため、他のフィーチャーブランチのデプロイを妨げることはありません。
2. `cancel-in-progress` の使い分け
- 本番デプロイ: `cancel-in-progress: true` を強く推奨します。万が一、デプロイが長引いたり、複数の開発者が同時に `main` にPushしたりした場合でも、古いデプロイを中断し、最新のコードでのデプロイを優先させます。
- ステージング/開発環境デプロイ: 状況によりますが、`true` が有効な場合が多いでしょう。しかし、デプロイプロセスが非常に長く、かつ「この順番で実行したい」という明確な意図がある場合は、`false` を検討しても良いかもしれません。ただし、その場合でも、デプロイが完了するまで新しいデプロイは待機するため、デプロイに時間がかかることへの注意は必要です。
3. デプロイメント戦略との連携
「Concurrency Group」は、あくまで「ワークフローの同時実行」を制御するものです。実際のデプロイメント戦略(Blue/Green Deployment, Canary Releaseなど)とは、連携して考える必要があります。
例えば、Blue/Green Deployment の場合、新しいバージョンへの切り替え処理自体がデプロイの一部となります。この切り替え処理が複数同時に走ってしまうと、予期せぬダウンタイムやロールバックの失敗につながりかねません。`concurrency` グループとデプロイメント戦略を組み合わせることで、より安全なデプロイを実現しましょう。
4. チーム内での周知とドキュメント化
どんなに優れた機能も、チームメンバーが理解していなければ宝の持ち腐れです。
- ワークフローの意図を共有: なぜ `concurrency` グループを設定しているのか、その目的(デプロイ事故防止、最新性の確保など)をチーム全体で共有しましょう。
- ドキュメント化: GitHubリポジトリの `README.md` や、チームのWikiなどに、CI/CDパイプラインの構成、特に `concurrency` の設定意図と運用ルールを明記しておきましょう。これにより、新しいメンバーもスムーズに理解できます。
まとめ:GitHub Actionsの「Concurrency Group」で、デプロイの迷いを断ち切る!
いかがでしたでしょうか?
GitHub Actionsの `concurrency` グループは、大規模開発におけるデプロイの競合という、まさに「現場で震える」ような課題を解決してくれる、非常に強力な機能です。
- `group` で排他制御の対象を定義し、
- `cancel-in-progress: true` で実行中の古いワークフローをキャンセルすることで、
- 常に最新かつ安全なコードをデプロイできる ように環境を整えられます。
これをマスターすれば、皆さんの毎日のデプロイ作業が、よりスムーズで、そして何よりも「安心」できるものに変わるはずです。
ぜひ、皆さんのプロジェクトでも「Concurrency Group」を活用して、デプロイ事故の不安から解放され、より生産性の高い開発ライフを送ってくださいね!
もし、さらに深掘りしたい点や、具体的な運用での疑問点があれば、いつでも気軽に質問してください。皆さんのCI/CDライフを全力でサポートします!