GitHub Actionsが遅い原因と実装レベルでの高速化テクニック5選

GitHub Actionsのワークフロー実行時間が長いと、デプロイやテストの効率が大きく低下します。本記事では、実際のプロジェクトで即座に適用できる5つの高速化テクニックを、具体的なコード例とともに解説します。これらの方法を組み合わせることで、ワークフローの実行時間を30~50%短縮することが可能です。

GitHub Actionsが遅くなる主な原因

ワークフローが遅くなる原因は、ランナー側の問題とワークフロー設計の問題に大別されます。まず原因を理解することが最適な対策につながります。

ランナーの起動時間と依存関係のインストール

GitHub Actionsのワークフローは、ジョブごとに新しいランナーインスタンスが起動します。初回起動時には、Node.jsやPythonなどの環境セットアップに1~2分かかることがあります。さらに、npm installやpip installといった依存関係のインストールも時間を消費します。

不要なステップの実行と並列化不足

複数のテストやビルドステップが順序に実行されている場合、これらを並列化することで実行時間を大幅に削減できます。また、条件分岐を活用して不要なステップをスキップすることも重要です。

テクニック1:キャッシュメカニズムの活用

actions/cacheを使用して、依存関係やビルド成果物をキャッシュすることが最も効果的な高速化方法です。npm modules、Python packages、Mavenリポジトリなど、変更頻度の低いファイルをキャッシュに保存します。

npm依存関係のキャッシング設定

以下は、Node.jsプロジェクトでnpm installの時間を短縮する実装例です。

name: CI with npm cache

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # キャッシュキーに package-lock.json のハッシュを含める
      - uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-
      
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - run: npm ci  # npm install ではなく npm ci を使用
      - run: npm run build
      - run: npm test

重要なポイントは、npm ci(クリーンインストール)を使用することです。npm installよりも高速で、再現性に優れています。

Python環境のキャッシング

Pythonプロジェクトでも同様にキャッシュを活用します。

- uses: actions/cache@v3
  with:
    path: ~/.cache/pip
    key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    restore-keys: |
      ${{ runner.os }}-pip-

- uses: actions/setup-python@v4
  with:
    python-version: '3.11'

- run: pip install -r requirements.txt

テクニック2:マトリックスジョブの最適化と並列実行

複数の環境でのテストが必要な場合、strategy.matrixを使用することで、これらを並列に実行できます。ただし、効率的に設計しないと無駄な実行が増えてしまいます。

テスト実行の並列化

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [16, 18, 20]
        test-suite: [unit, integration, e2e]
      # 複数ジョブが同時に実行され、全体の時間が短縮される
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      
      # test-suite の値に応じて異なるテストを実行
      - run: npm run test:${{ matrix.test-suite }}

条件付きジョブのスキップ

PR時には軽量なテストのみ実行し、本番ブランチへのマージ時に全テストを実行するパターンは、CI時間を大幅に削減します。

jobs:
  lightweight-test:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test:unit  # 軽量なユニットテストのみ

  full-test:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run test  # 全テストを実行

テクニック3:自己ホストランナーの活用

GitHub公式のランナーは一般的な構成に最適化されていますが、カスタムハードウェアを使用することでさらなる高速化が可能です。特に、大規模なビルドやメモリ集約的なテストでは効果的です。

自己ホストランナーの設定

自己ホストランナーを導入する際の基本的なワークフロー指定方法は以下の通りです。

jobs:
  build:
    runs-on: self-hosted  # GitHub公式のランナーではなく自社ランナーを使用
    # または特定のラベルを指定
    # runs-on: [self-hosted, linux, gpu]
    steps:
      - uses: actions/checkout@v4
      - run: ./build.sh
      - run: npm test

注意点:自己ホストランナーは管理とセキュリティ責任が増すため、オンプレミス環境やセキュリティ要件が高い場合のみ推奨します。一般的なプロジェクトではGitHub公式ランナーのキャッシング最適化で十分な高速化が期待できます。

テクニック4:不要なチェックアウトとビルド成果物の削減

git履歴の制限

fetch-depthパラメータを使用して、不要なgit履歴をダウンロードしないようにします。

- uses: actions/checkout@v4
  with:
    fetch-depth: 1  # 最新のコミットのみ取得(デフォルトは全履歴)
    # または、PR時のみ履歴を制限
    fetch-depth: ${{ github.event_name == 'pull_request' && 1 || 0 }}

ビルド成果物の条件付きアップロード

テスト失敗時にはアーティファクトをアップロードしない、またはPR時にはスキップするなどの工夫で、ストレージI/Oを削減できます。

- name: Upload test results
  if: failure()  # テスト失敗時のみアップロード
  uses: actions/upload-artifact@v3
  with:
    name: test-results
    path: ./test-results/
    retention-days: 7  # 7日後に自動削除

テクニック5:ワークフローファイルの構造最適化

複雑なワークフローの分割

大規模なワークフローは複数のファイルに分割し、workflow_callや条件分岐を活用して実行を制御します。

# .github/workflows/ci.yml
name: CI Pipeline

on: [push, pull_request]

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

  test:
    runs-on: ubuntu-latest
    needs: lint  # lint が成功した場合のみ実行
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

  build:
    runs-on: ubuntu-latest
    needs: test  # test が成功した場合のみ実行
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build

needsキーワードを使用することで、ジョブ間の依存関係を明示的に指定でき、不要な実行を防げます。

再利用可能なワークフロー

複数のリポジトリで共通するテストやビルドプロセスは、再利用可能なワークフロー(reusable workflow)として抽出します。

# .github/workflows/reusable-test.yml
name: Reusable Test Workflow

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        default: '18'
        type: string

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci
      - run: npm test

# メインワークフローから呼び出し
jobs:
  call-test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: '20'

実装時のハマりポイントと解決策

キャッシュキーの不適切な設定

問題:hashFiles()関数を使用しない場合、依存関係に変更があってもキャッシュが更新されず、古い依存関係を使用してしまいます。

解決策:必ずロックファイル(package-lock.jsonrequirements.txt)のハッシュをキーに含めてください。

# 良い例
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}

# 悪い例(キャッシュが更新されない)
key: ${{ runner.os }}-npm-latest

マトリックスの過度な拡大

問題:複数のNode.jsバージョン×複数のOSの組み合わせが増えると、実行ジョブ数が指数関数的に増加し、むしろ総実行時間が増える可能性があります。

解決策:必要最小限の組み合わせのみテストし、主要環境に絞り込みます。マイナーバージョンはスキップするなどの工夫をしてください。

よくある質問

A: リポジトリあたり5GBまで、組織全体では最大100GBまでのキャッシュが可能です(2024年時点)。超過した場合は、最も古いエントリから削除されます。詳細はGitHub公式ドキュメントを参照してください。

A: ハードウェアスペックに依存しますが、一般的なマシン(4コアCPU、16GB RAM)で30~40%の短縮が報告されています。ただし、セットアップと保守コストを考慮する必要があります。

A: strategy.matrixを使用した場合、各OSのテストは並列実行されるため、単一OSでのテスト時間とほぼ同等です。ただし、マトリックスのサイズが大きすぎるとキューイングが発生し、遅延する可能性があります。

まとめ

  • キャッシング:actions/cacheを活用し、hashFiles()で正確なキー管理を行う。npm dependenciesで40~60%の時間短縮が期待できる。
K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →