Article

🚦 GitHub Actionsのconcurrency入門:二重デプロイと古いCIを防ぐ実務設定

githubactions, github, ci, devops

GitHub Actionsを運用していると、短時間に複数回pushしたときに古いコミットのCIが最後まで走ったり、同じ環境へのデプロイが重なったりすることがあります。

そこで使えるのが concurrency です。concurrencyは「同じグループに属するworkflow/jobを同時に実行させない」ための仕組みです。

この記事では、単に設定例を載せるだけでなく、cancel-in-progress をいつ有効にするべきか、CIとデプロイでどう使い分けるかまで整理します。

まず結論

開発ブランチのCIなら、古い実行を止める次の設定が扱いやすいです。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

一方、本番デプロイでは「新しいpushが来たから実行中のデプロイを途中で止める」という設計が危険な場合があります。その場合は cancel-in-progress: false を基本に考えます。

concurrencyとは

通常、GitHub Actionsでは複数のworkflow runを並列に実行できます。

例えば次の順でpushしたとします。

commit A -> CI開始
commit B -> CI開始
commit C -> CI開始

処理時間によってはA・B・CのCIが同時に動きます。

テストだけなら無駄なActions時間が増える程度で済むこともありますが、デプロイ処理では問題が大きくなります。

Aをproductionへデプロイ
Bをproductionへデプロイ
Cをproductionへデプロイ

完了順が保証されない処理では、意図しない競合を起こす可能性があります。

concurrency を設定すると、同じconcurrency groupに属するworkflowまたはjobを同時に走らせないよう制御できます。

基本設定

name: CI

on:
  push:
    branches:
      - main

concurrency:
  group: ci-main
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

group は排他制御の単位です。

同じ ci-main に属する実行はconcurrencyの制御対象になります。

cancel-in-progress: true は、同じグループで現在実行中の古いrunもキャンセルする設定です。

groupを固定文字列だけにしない

例えば複数ブランチでCIを動かしているのに、次のようにするとします。

concurrency:
  group: ci
  cancel-in-progress: true

featureブランチAのCI中にfeatureブランチBへpushすると、同じ ci グループなので互いに影響する可能性があります。

そこでGitHubのcontextを使います。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

github.workflow はworkflow名、github.ref は実行対象のGit refです。

例えば概念的には次のように別グループになります。

CI-refs/heads/main
CI-refs/heads/feature/login
CI-refs/heads/feature/payment

これなら別ブランチのCIを誤ってキャンセルしません。

また、同じリポジトリ内の複数workflowで同じconcurrency group名を使うと、別workflow同士でも影響するため、github.workflow を含める設計は安全側です。

PRのCIで古い実行を止める

Pull Requestへ何度もpushするケースでは cancel-in-progress: true が特に便利です。

name: Pull Request CI

on:
  pull_request:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

例えばレビュー指摘を直して3回連続でpushした場合、基本的に確認したいのは最新コミットの結果です。

古いコミットの重いテストを最後まで実行する必要がなければ、キャンセルすることでCI時間を節約できます。

デプロイでは慎重にする

CIとデプロイでは考え方を分ける必要があります。

例えば本番デプロイを次のように設定したとします。

concurrency:
  group: production
  cancel-in-progress: true

新しいrunが来ると、実行中のデプロイがキャンセル対象になります。

デプロイ途中のキャンセルを安全に扱えない仕組みでは危険です。

そのため、本番では例えば次のようにします。

concurrency:
  group: production
  cancel-in-progress: false

重要なのは「CIだからtrue、デプロイだからfalse」と機械的に決めることではなく、途中キャンセルしても処理が安全かを判断することです。

workflow全体ではなくjobだけ制御する

concurrency はworkflowレベルだけでなくjobレベルにも指定できます。

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm test

  deploy:
    needs: test
    runs-on: ubuntu-latest
    concurrency:
      group: production
      cancel-in-progress: false
    steps:
      - run: ./deploy.sh

この構成ならテストは通常どおり実行しつつ、productionへのdeploy jobだけを直列化できます。

「並列化できる処理まで全部止めたくない」という場合に有効です。

pendingの挙動にも注意する

concurrency groupを使うと、同じグループでは同時実行が制限されます。

GitHubの公式ドキュメントでは、通常は同じグループで最大1つがrunning、1つがpendingとなり、新しいpendingが来ると既存pendingが置き換えられる動作が説明されています。

そのため、concurrencyは単純な「無制限のFIFOキュー」と同じではありません。

「すべての実行を必ず順番に処理したい」という要件では、利用しているGitHub Actionsの現在のqueue設定を含め、公式仕様を確認して設計する必要があります。

よくあるハマりどころ

1. 全ブランチで同じgroupを使う

concurrency:
  group: build

これでは無関係なブランチまで同じ排他グループになります。

ブランチ単位なら次のようにします。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}

2. 本番デプロイに無条件でcancel-in-progressを付ける

cancel-in-progress: true

デプロイ処理が途中キャンセルに対応しているか確認します。

DBマイグレーションや外部サービス更新など、途中停止で状態が中途半端になる処理があるなら特に注意が必要です。

3. concurrencyをロック機構だと思い込む

concurrencyはGitHub Actions上のworkflow/job実行を調整する仕組みです。

アプリケーション側の分散ロックやDBの排他制御そのものを置き換えるものではありません。

例えば複数の別システムから同じDB更新処理を呼び出せるなら、アプリケーション側でも整合性を守る必要があります。

CIとデプロイの使い分け

目安としては次のように考えられます。

用途 group例 cancel-in-progress
PRのテスト ${{ github.workflow }}-${{ github.ref }} true
featureブランチのCI ${{ github.workflow }}-${{ github.ref }} true
mainのビルド ${{ github.workflow }}-${{ github.ref }} 要件次第
stagingデプロイ staging 要件次第
productionデプロイ production 原則、途中キャンセルの安全性を確認して決める

まとめ

GitHub Actionsで短時間に複数回pushされる環境では、concurrency を設定するだけで不要な古いCIやデプロイ競合を減らせます。

特にPRのCIでは、次の形から始めると扱いやすいです。

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

一方、productionデプロイでは「最新だけ残せばよい」とは限りません。途中キャンセル時の安全性を確認したうえで cancel-in-progress を決めることが重要です。

参考