GitHub Actions を高速化した3つの考え方

公開日:

ある日 GitHub Actions が失敗していた。。

GitHub Actions の実行結果が表示されるページにThis job failedと表示されていました。

Re-run jobsしてもジョブは失敗したまま。よく見ると、エラーメッセージが。

The job was not started because recent account payments have failed or your spending limit needs to be increased. Please check the 'Billing & plans' section in your settings.

GitHub Actions の無料枠を使い切っていました。とりあえず追加料金を払う設定をしてエラーは解決しました。

ですが、ちょうど最近 GitHub Actions の実行が終わるまで20分以上かかるようになっていて、待つのがしんどくなってきていた所だったので改善に取り組むことにしました。

結果、 21分かかっていた処理が、12分程度 まで短縮できました。この記事はその時の記録です。

GitHub Actions の構成

運用していた GitHub Actions の構成です。

  1. Nuxt3 をセットアップ(npm ci等)
  2. ユニットテスト(Vitest)
  3. e2e テスト(Playwright)
  4. Dockerfile からイメージをビルド
  5. Cloud Run にアップロード

やったこと

  1. 使ってないライブラリを依存関係から削除
  2. node_modulesをキャッシュ
  3. job の並列実行

1. 使ってないライブラリを依存関係から削除

GitHub Actions では基本的に毎回依存関係を都度ダウンロードする必要があります。キャッシュを使っていたとしても初回は必ずダウンロードします。

通信の無駄なので、以前使っていて今は使っていないライブラリがpackage.jsonに残っていたのでアンインストールしました。

npm uninstall [使っていないパッケージ名]

2. node_modulesをキャッシュする

次はnode_modulesのデータをキャッシュします。次回からキャッシュから復元することで、npm ciするより早くnode_modulesを構成できます。

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
          
      - name: 1. node_modules をキャッシュする
        id: cache-npm-node-modules
        uses: actions/cache@v3
        env:
          cache-name: node-modules
        with:
          path: "node_modules"
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('package-lock.json') }}

      - name: 2. キャッシュがなかったら npm ci を実行
        if: steps.cache-npm-node-modules.outputs.cache-hit != 'true'
        run: |
          npm ci

ポイント 1. node_modules をキャッシュする

actions/cacheを使用してデータをキャッシュします。

  • id: Step2で Step1の結果を取得
  • env
    • cache-name: key内で使用
  • with
    • path: キャッシュするディレクトリを指定
    • key: キャッシュした時のディレクトリ名

actions/cachekeyに合致するキャッシュがあったら、キャッシュから復元します。keyhashFiles('package-lock.json')を含めることで、package-lock.jsonの変更、つまり依存関係の変更を検知します。

keyに合致するキャッシュがなかったらactions/cacheはキャッシュを復元しません。

ポイント 2. キャッシュがなかったら npm ci を実行

npm cinode_modulesがあったとしても依存関係を再ダウンロードします。なので、実行時間を短縮するためにはキャッシュがあった場合はnpm ciを実行したくありません。

  1. node_modulesがあった場合はnpm ciを実行しない
  2. node_modulesがなかった場合はnpm ciを実行する

という挙動をifを使って作っています。

そうすることで初回実行時や、依存関係に変更があった時だけnpm ciを実行しています。

5. job の並列実行

元々、ユニットテストと、e2e テストを直列で実行していましたが、並列で実行するように変更しました。

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

    # 依存関係のインストール等
    # 依存関係をキャッシュ

  test-unit:
    needs: [setup]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

    # 依存関係を復元
    # ユニットテストを実行

  test-e2e:
    needs: [setup]

    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

    # 依存関係を復元
    # e2e テストを時効

  deploy:
    needs: [setup, test-e2e, test-unit]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2

    # 依存関係を復元
    # ビルドしてデプロイ

ポイント

needsでジョブが実行されるまでに完了している必要があるジョブを定義しています。例えばdeployジョブはsetuptest-e2etest-unitが正常に終わっていないと実行されません。

また、各ジョブではインストールしたnode_modulesなどは引き継げないので、キャッシュして復元する必要があります。

さいごに

全部の対策を行なった結果、 21分かかっていた処理が12分程度まで短縮できました!