♻️

フロントエンドエンジニアが知っておきたいCI/CD

に公開

フロントエンドエンジニアのみなさん、CI/CD はどれくらい活用されているでしょうか?
私の現場では、ユニットテスト・静的解析・VRT・E2E テストを CI で回しています。

これらの仕組みは一度構築してしまえば機能開発のようにコードの変更が必要になる場面が少ないのがメリットである反面、日常的に触らない分知見の得られにくさがあります。
また、主に HTML・CSS・JavaScriptを主に扱うフロントエンドエンジニアとして CI/CD は取っ付きにくさがあります(あくまで個人的に)

しかし、依存関係の更新などで CI/CD が落ちた場合に調査が必要になる場合や、コードベースが大きくなるにつれてCIの実行時間が長くなるといった問題に対処するのもフロントエンジニアの役割です。
CI/CD の仕組みを知らないと、いざというときにあたふたしますし、私はいまだに予期せぬ時に CI が落ちるとドキッとしてしまいます。

そのため、フロントエンドエンジニアが CI/CD を触るにあたって知っておきたい知識をまとめます。
リリース時に CI が落ちませんように 🙏と祈るだけにするのはもうやめにしましょう。

CI を実行する環境を知る

主な CI ツールは Docker コンテナで実行されます。
必要に応じて、Docker に CI を実行するための環境を準備する必要があります。

まずは 目的に応じて環境を整えます。
フロントエンドで CI で実行することは主に ユニットテスト・静的解析・VRT・E2E テストが挙げられると思います。

Jest などのユニットテストや TypeScript や ESLint などの静的解析では Node.js が必要ですし、VRT や E2E テストにはブラウザが必要です。

テストの種類 主なツール CIで必要な環境
ユニットテスト jest・vitest・testing-library Node.js
静的検査 TypeScript・ESLint・Biome Node.js
VRT chromatic・storycap・playwright ブラウザ
E2E テスト playwright・cypress・puppeteer ブラウザ、バックエンドモック or 実環境接続

Node.js 環境の用意

Node.js 環境を用意するには Node.js を含むベースイメージを使うのが一般的です。

Node.js 本体だけでなく、yarn などのパッケージマネージャーも同梱されており、バージョンの指定方法も簡単です。
また、ビルド高速化のためのキャッシュ機能など、Node.js 開発環境を整える上で必要な機能が一つにまとまっています。

GitHub Actions の setup-nodeの例

steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
  with:
    node-version: 18
- run: npm ci
- run: npm test

CIで使うブラウザについて

CI 環境では GUI がないヘッドレスブラウザを使用します。
直接人間が操作するのではなく、自動テストが目的であるため、動作が軽量なヘッドレスブラウザが最適です。
代表的なヘッドレスブラウザには Headless Chrome ・Headless Chromium があります。

ブラウザテスト自動化のフレームワーク

ヘッドレスブラウザをテストに利用するには、プログラムでブラウザを操作できるフレームワークを使うのが一般的です。
代表的なフレームワークには Puppeteer・Playwright・Cypress があります。

これらのフレームワークは、インストール時に対応するヘッドレスブラウザを自動的にダウンロードまたは同梱しており、別途ブラウザを手動でインストールする必要は基本的にありません。

ただし、Playwright ではplaywright installで明示的にインストールする必要があります。

知っておくべきCI/CDの概念

ワークフロー

ワークフローはジョブの組み合わせとその実行順序を定めたルールです。
ワークフローによってビルド・テスト・デプロイなどのプロセスを組み合わせることができます。

順次実行と並列実行

ジョブは順次または並列で実行できます。
順次実行するジョブをシーケンシャルジョブと呼びます。
シーケンシャルジョブはジョブ間での依存が必要な場合に設定します。

例えば、test ジョブには buildジョブ、deployジョブにはtest ジョブの成功が必要というようにジョブ間の依存関係をコントロールします。

並列実行では複数のジョブを同時に実行します。
特定のジョブが終わった後に複数のジョブを並列実行することをファンアウト、並列実行しているジョブが全て完了してから共通のジョブを実行することをファンインと呼びます。

データの共有

ジョブで作成したデータはジョブ終了とともに消滅するため、ジョブ間でデータを共有するには特定のストレージへ一時的に保存する必要があります。

GitHubActions ではアーティファクト、CircleCI ではワークスペースと呼ばれるストレージにジョブ間での成果物を共有します。

主に npm install で生成した node_modules をジョブ間で共有する用途で使われます。

node_modules など依存関係は cache を使うこと次回以降のジョブを高速化できます。

また、依存関係をインストールする場合はチェックアウトしたコードの lockファイルとバージョンを完全一致させることでバージョン違いによる予期せぬバグを防ぎます。

具体的には、npm の場合は npm ci コマンド、yarn の場合は yarn install --frozen-lockfile コマンドで lockファイルのバージョンを固定します。

CI の速度改善について

基本的な CI の速度改善手法について説明します。

Cache の活用

node_modules などの依存関係のインストールは cache を活用することで実行時間を短縮できます。

CI の初回実行時に node_modules の cache を保存しておき、lockファイルに変更がなければ cache をそのまま使用します。

また、npm などのパッケージマネージャは変更があったパッケージのみ部分的に更新する仕組みであるため、CI の cache も部分的に更新する設計にしておくことで、変更があった際にもインストールを高速化できます。

restore_cache:
  keys:
    - v1-dependencies-{{ checksum "yarn.lock" }} #① 
    - v1-dependencies- #②

v1-dependencies-{{ checksum "yarn.lock" }} では yarn.lock ファイルのハッシュ値(checksum)を計算します。
これにより、yarn.lock が変更されるたびに異なるキャッシュキーが生成され、新しい依存関係をキャッシュできます。v1-dependencies- はプレフィックスとしてキャッシュのバージョン管理に使用します。

v1-dependencies- は①のキーが見つからない場合にv1-dependencies-に前方一致するキャッシュキーが存在すればそれを復元し、キャッシュされているパッケージを再利用します。
これにより、すべてのパッケージをゼロからインストールせずに、キャッシュされていないパッケージのみインストールを実行します。

並列実行

並列実行には 「ジョブを並列実行する方法」「ジョブ内の処理を並列実行する方法」 の2つがあります。
ジョブを並列実行する方法では、複数のジョブを並列に実行することでCIの速度を改善します。

ジョブ内で処理を並列実行する方法では、1つのジョブを複数のコンテナで分散して実行することで実行時間を短縮します。
例えば、playwright にはテストを分割して実行する Sharding という機能があるので、分割したテストを環境を分割して実行することでCIの速度を改善できます。
これは、それぞれのテストが依存していない場合に有効です。

おわりに

既に構築されている CI を触る機会はあまりないだろうと考えていましたが、並列化や cache 設計の最適化による高速化など、見直せる部分はありそうだなと感じました。
CI が通るのを待っている時間は別の作業を進めるにも中途半端な時間になりがちなので、ログを眺めて改善調査に当てる機会にしていこうと思います。

Discussion