はじめに
Github Actions の Workflow を高速化する方法を解説します。
解説はフロントエンドのデプロイワークフローを例にしますが、他にも転用できると思います。
前提
実行環境として Node.js、パッケージマネージャに Yarn を使ったプロジェクトを想定をします。
フロントエンドでは、デプロイするまでに行うこととして、主に以下の事項があります。
- モジュールのインストール
- リンターによる静的チェック
- 各種テスト(Unit, E2E)
- ビルド
- デプロイ
基本的には、これらを事項について、ワークフローで上から行っていけばいいわけですが、プロジェクトが大きくなると、パッケージのインストールは遅くなり、静的チェックも時間がかかるようになります。よってこれらを高速化することを目指しましょう。
モジュールのインストールを高速化する
モジュールのインストールを高速化することは、手軽かつ確実に速度に繋がります。他の手法を差し置いても、これだけは是非しておくといいと思います。
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つあり、
- Github の公式 Action である
actions/cache
を使い、node_modules をキャッシュすること。 - パッケージのインストール時に
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-hit
は true を返します。
また、自動的にリストアが行われるので、同一 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の中の一人、宮内さんに書いていただきました。