GitHub Actions高速化実践:Matrix戦略・依存キャッシュ・失敗切り分けの設計ガイド

GitHub Actions は便利ですが、プロジェクトが成長すると「遅い」「不安定」「原因が分かりにくい」という三重苦になりがちです。特に monorepo や複数ランタイム対応(Node/Python/Go など)では、ワークフローの設計次第で CI 時間が 2〜3 倍変わります。

本記事では、実行時間を短くしながら失敗時の調査コストも下げるために、matrix 設計・キャッシュ設計・障害時の確認順序を具体的に整理します。

1. まず「何を並列化するか」を決める

Actions の高速化は、いきなりキャッシュ最適化から入るより、先にジョブ分解を決める方が効きます。原則は次の通りです。

  • 並列化すべき: 独立テスト(OS/バージョン別、サービス別)
  • 直列にすべき: デプロイ、DB マイグレーション、本番反映
  • 依存を分ける: lint/typecheck/test/build を一つに詰め込まない

悪い例は、1ジョブに全部詰め込み、失敗時に最初から再実行するパターンです。良い設計では「lint は通るが test だけ失敗」のように切り分けできます。

2. matrix を作るときの実践ルール

matrix は便利ですが、組み合わせ爆発で逆に遅くなることがあります。例えば os x runtime x db をすべて直積にすると、不要なジョブが大量発生します。そこで include/exclude を活用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
strategy:
  fail-fast: false
  matrix:
    os: [ubuntu-latest, macos-latest]
    node: [20, 22]
    include:
      - os: ubuntu-latest
        node: 22
        coverage: true
    exclude:
      - os: macos-latest
        node: 20

ポイントは次です。

  1. 基準環境を1つ決める(例: ubuntu + latest)
  2. カバレッジ計測や重い E2E は基準環境だけで実施
  3. 互換性確認は軽量テスト中心にする

この設計にすると、品質を落とさずに全体時間を短縮できます。

3. キャッシュは「鍵設計」が9割

actions/cachesetup-node / setup-python のキャッシュを入れても、キー設計が甘いとヒット率が低く、逆に復元時間だけ増えます。

Node.js の例:

1
2
3
4
5
6
7
- uses: actions/setup-node@v4
  with:
    node-version: ${{ matrix.node }}
    cache: 'npm'
    cache-dependency-path: |
      package-lock.json
      packages/*/package-lock.json

Python (pip) の例:

1
2
3
4
5
6
7
- uses: actions/setup-python@v5
  with:
    python-version: '3.12'
    cache: 'pip'
    cache-dependency-path: |
      requirements.txt
      requirements-dev.txt

実務で効くコツ:

  • lockfile をキーに含める(依存変化に追従)
  • OS・ランタイムバージョンをキーに含める
  • monorepo は対象サブディレクトリ単位でキー分割
  • restore-keys を入れすぎない(古いキャッシュ復元で不整合)

4. concurrency で「古い実行を止める」

PR に連続 push されると、古い CI が残り続けてランナー枯渇を起こします。concurrency を入れて、最新コミットだけ走らせる構成にします。

1
2
3
concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

これだけで無駄実行を大きく減らせます。特にレビュー中に細かい修正を重ねるチームほど効果が高いです。

5. paths-filter で不要ジョブを起動しない

ドキュメント更新だけなのに全テストが走る、という状態はよくあります。dorny/paths-filter で変更範囲に応じてジョブを分岐します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- uses: dorny/paths-filter@v3
  id: changes
  with:
    filters: |
      backend:
        - 'backend/**'
      frontend:
        - 'frontend/**'

# 例: backend が変わった時だけ実行
if: steps.changes.outputs.backend == 'true'

これにより、実行時間だけでなくランナーコストも下げられます。

6. 失敗時の調査を速くするログ設計

CI が遅い組織は、だいたい「失敗調査も遅い」です。改善するには、次の3点を標準化します。

  • 失敗したジョブで artifact(ログ、スクリーンショット、coverage)を必ず保存
  • 重要ステップに ::group:: を付けてログを畳む
  • flaky テスト検出用に rerun 情報を残す

artifact 例:

1
2
3
4
5
6
7
8
- name: Upload test reports
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: test-report-${{ matrix.os }}-${{ matrix.node }}
    path: |
      reports/
      coverage/

if: always() を忘れると、失敗時ほど証跡が残らないので注意です。

7. self-hosted runner を使う場合の注意点

高速化目的で self-hosted runner を導入する場合、運用事故が増えやすい領域です。

  • 毎回クリーンワークスペース化(残骸で再現不能バグ)
  • シークレットを runner に永続化しない
  • パッチ適用・再起動の定期メンテをスケジュール化
  • runner ラベルを用途別に分離(deploy と test を混在させない)

また、デプロイ権限を持つ runner と、PR 由来コードを実行する runner は分離するのが基本です。

8. 実際の改善ステップ(2週間)

Day 1-2: 現状計測

  • 平均実行時間、p95 実行時間、失敗率を取得
  • 一番遅いジョブ上位3つを特定

Day 3-5: ジョブ分割と matrix 整理

  • lint/typecheck/test/build を分離
  • matrix の組み合わせを include/exclude で整理

Day 6-8: キャッシュ最適化

  • lockfile ベースキーへ統一
  • キャッシュヒット率を可視化

Day 9-10: 無駄実行削減

  • concurrency + cancel-in-progress
  • paths-filter で対象限定

Day 11-14: 運用ルール化

  • 失敗時 artifact を全ジョブ標準化
  • flaky テスト記録のテンプレート化

この手順で進めると、速度改善だけでなく再発防止まで一気に整います。

9. よくある失敗パターン

パターンA: キャッシュを入れたのに遅い

原因:

  • キーが細かすぎて毎回ミスヒット
  • 圧縮/復元コストが大きいディレクトリを丸ごとキャッシュ

対処:

  • 依存に限定してキャッシュ
  • キーに lockfile ハッシュを利用

パターンB: matrix 失敗がノイズ化

原因:

  • fail-fast: true で他環境の情報が取れない
  • ログ命名が統一されず比較困難

対処:

  • fail-fast: false
  • artifact 命名規則を統一

パターンC: PR の待ち時間が長い

原因:

  • 古いコミットの CI が走り続ける
  • 変更範囲に関係ないジョブが常時起動

対処:

  • concurrency で古い実行を停止
  • paths-filter 導入

10. 運用チェックリスト

  • ジョブは責務別に分離されている
  • matrix は必要最小限に絞られている
  • 依存キャッシュのキーに lockfile が含まれる
  • concurrency で古い実行をキャンセルしている
  • 変更範囲に応じたジョブ起動制御がある
  • 失敗時 artifact が必ず残る

GitHub Actions は「機能を使う」だけでは速くなりません。実行単位の設計、キャッシュ鍵設計、無駄実行抑制、証跡設計をセットで行うと、初めて安定した CI 基盤になります。