CI/CDのボトルネックを「キャッシュ戦略」で粉砕せよ:npm/yarn/pnpmの深層最適化
CI/CDパイプラインにおいて、`npm install` や `pnpm install` に費やす時間は、単なる待機時間ではない。それは「エンジニアの認知コスト」と「計算リソースの浪費」という、ビジネスにおける二大損失である。
多くのエンジニアは `actions/setup-node` をなんとなく使っている。しかし、真のDevOpsアーキテクトであれば、「なぜキャッシュが効かないのか」「なぜロックファイルのハッシュだけでは不十分なのか」という深淵まで理解していなければならない。
今日は、GitHub Actionsにおけるパッケージマネージャーのキャッシュ戦略を、単なる「設定値」のレベルから、パイプラインのアーキテクチャ設計というレベルまで引き上げて解説する。
—
1. キャッシュの真実: `setup-node` の裏側と「パス」の支配
`actions/setup-node` は強力だが、ブラックボックスではない。内部では `actions/cache` をラップし、指定された `cache-dependency-path` を元にキーを生成している。
ここで最も陥りやすい罠が、「Dockerコンテナ内でのキャッシュパスの不整合」だ。
Docker環境におけるキャッシュの永続化戦略
多くの現場で、Dockerコンテナ内でビルドを実行する際、キャッシュパスをローカル環境と同期できずにキャッシュミスを連発している。解決策はシンプルだ。コンテナのボリュームをGitHub Actionsのホスト環境と共有し、パスを固定する。
GitHub ActionsのYAML設定例
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ’20’
# キャッシュ対象を明示し、コンテナ内でもこのパスをマウントさせる
cache: ‘pnpm’
cache-dependency-path: ‘/pnpm-lock.yaml’
# pnpmの場合、store-dirを強制的にワークスペース配下に置くのが鉄則
- name: Setup pnpm config
run: pnpm config set store-dir .pnpm-store
なぜこれが必要か?
`pnpm` はコンテンツアドレス可能なストレージ(CAS)を使用する。デフォルトの `~/.pnpm-store` はCI環境のコンテナ再生成ごとに揮発するリスクが高い。明示的にワークスペース配下に置くことで、`actions/cache` が確実にそのディレクトリをスナップショットとして保存できるようになる。
—
2. pnpmを極める:Lockfileの非決定性を排除せよ
pnpmの強みは「硬いロックファイル」にあるが、CIではこれが裏目に出ることがある。特に `postinstall` スクリプトの実行や、OS依存のバイナリが絡む場合、キャッシュの復元に失敗することが多い。
アーキテクトの推奨: `pnpm fetch` の活用
`pnpm install` を叩く前に、まず `pnpm fetch` を実行せよ。これにより、パッケージの物理的なダウンロードとインストール作業を分離できる。
キャッシュがある状態で実行すると、ネットワーキングをバイパスし、
キャッシュの整合性チェックのみを行うため、爆速で完了する
pnpm fetch –frozen-lockfile
その後、実際のリンク処理を行う
pnpm install –offline –frozen-lockfile
これにより、CIのログには「ダウンロードが完了しました」ではなく「キャッシュから即座にリンクが生成されました」という記録が残るようになる。この切り分けこそが、大規模モノレポにおけるビルド高速化の鍵だ。
—
3. 独自のキャッシュ戦略: `actions/cache` を直接叩くメリット
`setup-node` が提供する抽象化は便利だが、高度な最適化を求めるなら、自分でキャッシュキーを制御せよ。
例えば、`package.json` の `dependencies` だけではなく、「OSのバージョン」や「Node.jsのマイナーバージョン」をキーに組み込むことで、キャッシュの汚染(Pollution)を防ぐ。
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.pnpm-store
# キーにOSとNodeバージョンを含めることで、環境移行時の事故を皆無にする
key: ${{ runner.os }}-node-${{ hashFiles(‘/pnpm-lock.yaml’) }}
restore-keys: |
${{ runner.os }}-node-
ここがプロの視点:
`restore-keys` を過信してはいけない。常に「完全一致」を狙うのが基本だ。もし `restore-keys` が多すぎると、古いキャッシュから不整合な `node_modules` が復元され、デバッグ不能なビルドエラーを引き起こす。キャッシュは「一撃必殺(Perfect Match)」を目指すべきであり、「とりあえず動く」ための救済措置にしてはならない。
—
4. 究極の最適化:不要なビルドを「追放」する
キャッシュ最適化の究極形は、「キャッシュをどれだけ速く戻すか」ではなく、「そもそもインストールをどれだけスキップできるか」である。
CIのパイプラインで `git diff` を活用し、特定のディレクトリに変更がない場合は、そもそもパッケージの更新自体をスキップする設計を導入せよ。
変更があったパッケージのみを特定する独自スクリプトの例
pnpmのフィルタリング機能と組み合わせる
if git diff –quiet HEAD^ HEAD — packages/ui; then
echo “UIパッケージに変更なし。インストールをスキップ。”
exit 0
fi
—
結びに代えて:アーキテクトの矜持
キャッシュ設定とは、単なるYAMLの記述ではない。それは、「ソフトウェアの依存関係という複雑極まりないグラフを、いかに確定的な時間内に再現するか」という、CI/CDの心臓部を設計する作業だ。
今日紹介した設定を、あなたのプロジェクトのパイプラインに組み込んでほしい。ビルド時間が数分短縮されるだけで、一ヶ月後にはチーム全体で何十時間もの「思考の連鎖」を救い出すことになる。
エンジニアリングとは、効率の追求そのものである。妥協なき設計を続けよう。