LoginSignup
57
20

More than 3 years have passed since last update.

GitHub Actions の Workflow を高速化する

Posted at

はじめに

Github Actions の Workflow を高速化する方法を解説します。
解説はフロントエンドのデプロイワークフローを例にしますが、他にも転用できると思います。

前提

実行環境として Node.js、パッケージマネージャに Yarn を使ったプロジェクトを想定をします。
フロントエンドでは、デプロイするまでに行うこととして、主に以下の事項があります。

  1. モジュールのインストール
  2. リンターによる静的チェック
  3. 各種テスト(Unit, E2E)
  4. ビルド
  5. デプロイ

基本的には、これらを事項について、ワークフローで上から行っていけばいいわけですが、プロジェクトが大きくなると、パッケージのインストールは遅くなり、静的チェックも時間がかかるようになります。よってこれらを高速化することを目指しましょう。

モジュールのインストールを高速化する

モジュールのインストールを高速化することは、手軽かつ確実に速度に繋がります。他の手法を差し置いても、これだけは是非しておくといいと思います。

Node.js プロジェクトでは、パッケージマネージャに Yarn、Yarn2 や NPM 、pnpm など利用しますが、基本的にはどの言語のパッケージマネージャでも高速化の恩恵を預かれると思います。

以下、インストールのワークフローの例です。

name: deploy
on: push

jobs:
  setup:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [latest]
        node: [latest]
    steps:
      - uses: actions/setup-node@v2-beta
        with:
          node-version: ${{ matrix.node }}

      - name: Checkout
        uses: actions/checkout@v2

      - name: Cache node_modules
        id: node_modules_cache_id
        uses: actions/cache@v2 // 1
        with:
          path: node_modules
          key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}

      - name: Install
        if: steps.node_modules_cache_id.outputs.cache-hit != 'true' // 2
        run: yarn --check-files --frozen-lockfile --non-interactive

ポイントは2つあり、

  1. Github の公式 Action である actions/cache を使い、node_modules をキャッシュすること。
  2. パッケージのインストール時に actions/cache の戻り値を確認すること。

です。1 では、キャッシュのkeyとして yarn.lock ファイルのハッシュ値を使っています。一意であればなんでも構いませんが、matrix.osなどの環境情報を組み合わせるといいでしょう。

そして、yarn.lock ファイルの内容に変更があった、もしくは初回のワークフロー時には、

Run actions/cache@v2
Cache not found for input keys: ubuntu-latest-node-v12-deps-xxxxxxxxxxxxx

となり、actions/cacheの戻り値は false が返ります。また、actions/cacheの戻り値は cache-hit というキーに Boolean が入ります。取得するにはsteps.node_modules_cache_id.outputs.cache-hitのようにします。

反対に yarn.lock ファイルに変更がなければ、

Cache restored from key: ubuntu-latest-node-v12-deps-xxxxxxxxxxxxx

となり、steps.node_modules_cache_id.outputs.cache-hittrue を返します。

また、自動的にリストアが行われるので、同一 steps 内で、他の step を行う場合は、モジュールがインストールされた状態と同じになります。分割された job でモジュールのキャッシュを利用する場合は、追って解説します。

2 では、if の条件として、steps.node_modules_cache_id.outputs.cache-hitを指定しています。つまり、キャッシュが有った場合は、モジュールのインストール自体をスキップしています。

以上によって、変更ない場合のモジュールのインストールを回避し、ワークフローの高速化をすることができます。

余談ですが、キャッシュのポストは、steps の終わりに自動的に行われます。
つまり、上記のワークフローであれば、

Cache node_modules
Install
Post Cache node_modules

のようになり、自動的にキャッシュのポストが割り込まれます。キャッシュヒットした場合は、キャッシュのポストは行われません。

step の並列化で高速化する

step を job に分割することで、待ち時間を短縮し高速化が期待できます。Github Actions では job はランナーごとの単位であり、並列に動作します。そのため、例えばリントとテストのアクションは別の job にすることで、並列化することができます。

jobs:
  lint-script:
    needs: setup
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [latest]
        node: [latest]

    steps:
      - uses: actions/setup-node@v2-beta
        with:
          node-version: ${{ matrix.node }}

      - name: Checkout
        uses: actions/checkout@v2

      - name: Restore node_modules // 4
        id: node_modules_cache_id
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}

      - name: lint by ESlint
        run: yarn lint:script

  test-unit:
    needs: setup
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [latest]
        node: [latest]

    steps:
      - uses: actions/setup-node@v2-beta
        with:
          node-version: ${{ matrix.node }}

      - name: Checkout
        uses: actions/checkout@v2

      - name: Restore node_modules // 4
        id: node_modules_cache_id
        uses: actions/cache@v2
        with:
          path: node_modules
          key: ${{ matrix.os }}-node-v${{ matrix.node }}-deps-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}

      - name: Test unit
        run: yarn test:unit --ci

  build:
    needs: [lint-script, test-unit] // 3

リントとテストの終了を待つには、3 のように jobs.<job_id>.needs に job を指定する必要があります。

ここで、モジュールの利用法について触れます。job に分割したとき、各 step がモジュールを利用する場合には、step のはじめにモジュールをインストールするか、キャッシュからモジュールのリストアをする必要があります。上記で、モジュールをキャッシュしているので、それをリストアして使いましょう。

4 では、キャッシュのリストアをしています。見るとわかりますが、キャッシュのストアとリストアは全く同じインターフェイスになっています。

まとめ

ワークフローの高速化には、モジュールのキャッシュ化と、step の並列化によって達成できます。

step を job に分割するのは有効ですが、時間がかからない step の場合は、モジュールのリストアのほうが時間がかかってしまい、かえってワークフローが遅延してしまう可能性があります。また、現在の Github Actions では、 Free Plan アカウントの job の同時実行が 20 個までとなっています。

そのため、各 step を見定めて並列化を行うといいと思います。それでは。

*本記事は @qualitia_cdevの中の一人、宮内さんに書いていただきました。

57
20
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
57
20