CI/CDを「止まらないインフラ」へ昇華させる:npm/pnpmの脆弱性を克服するネットワーク・レジリエンス戦略
DevOpsの現場において、`npm install` や `pnpm install` がCIパイプラインの「おみくじ」と化している状況ほど、開発者の生産性を殺ぐものはない。ネットワークの一瞬の瞬断、レジストリのパケットロス、あるいはGitHub Actionsの共有ランナーにおける帯域制限。これらは「運が悪かった」で済ませるべき事象ではなく、アーキテクトが設計によって排除すべき不確実性である。
本稿では、レジストリのリトライ戦略から、コンテナ最適化、さらには透過的なフェイルオーバー実装まで、CIのビルド時間を「安定的かつ最短」にするための極意を伝授する。
—
1. なぜデフォルト設定ではCIで死ぬのか:低レイヤからの考察
npmやpnpmのデフォルトのネットワーク設定は、開発者が自宅の安定したWi-Fi環境で行う `npm install` を前提としている。しかし、CI環境は全くの別物だ。
- DNSのレイテンシとTCP接続: パッケージ数が多いプロジェクトでは、数百のTCPコネクションが同時に開かれる。DNS解決のタイムアウトが連鎖すると、一気にパイプラインが停止する。
- レジストリの「不機嫌」: npmレジストリ(registry.npmjs.org)は世界規模のCDNで保護されているが、特定のリージョンやタイミングで応答が遅延する。デフォルトのタイムアウト値(多くの場合、固定されているか、極めて短い)では、わずかな遅延が致命的な `ETIMEDOUT` を引き起こす。
我々が最初に行うべきは、「待つこと」を戦略的に定義することだ。
—
2. CI環境における「鉄壁」の設定術
設定は `.npmrc` または環境変数で行う。CI環境では、環境変数を優先させるのがベストプラクティスだ。
タイムアウトの引き上げ:デフォルトの短さを克服
ネットワークの揺らぎを許容するために 60秒(60000ms) 以上を推奨
export NPM_CONFIG_TIMEOUT=60000
リトライ回数の最適化:デフォルトの2回では足りない
5回まで再試行を許可することで、一時的なパケットロスを透過的に解決する
export NPM_CONFIG_FETCH_RETRIES=5
リトライ間のバックオフ時間(指数関数的増加)
ネットワーク負荷を考慮し、連続アクセスを避ける
export NPM_CONFIG_FETCH_RETRY_FACTOR=10
最小速度の指定
ネットワークが遅い場合に即座に切断せず、粘り強くダウンロードを継続する
export NPM_CONFIG_FETCH_RETRY_MINTIMEOUT=20000
これらの設定をCIパイプラインの冒頭で注入することで、パケットロスによるビルド失敗を劇的に削減できる。
—
3. 透過的フェイルオーバー:レジストリの冗長化
npmレジストリが完全にダウンした場合、どんなにリトライ設定をしても無駄だ。ここで、「プライマリレジストリ+セカンダリレジストリ」の冗長化構成を導入する。
残念ながら、npm自体には「AがダメならB」というネイティブなフェイルオーバー機能はない。しかし、`verdaccio` などの軽量キャッシュサーバーをプロキシとして挟むか、pnpmの機能でレジストリを複数参照することでこれを補完できる。
pnpmによるマルチレジストリ戦略
pnpmは `.npmrc` でスコープごとにレジストリを定義できるため、これを利用して冗長化を図る。
.npmrc
デフォルトはメインのレジストリ
registry=https://registry.npmjs.org/
社内プライベートレジストリやミラーを優先したい場合はスコープで制御
@my-scope:registry=https://registry.verdaccio.internal/
ネットワーク不安定な環境向けに、CI上でのみ環境変数で上書きする設計
下記はCI環境で動的に書き換えるためのフックとしての記述
真のアーキテクトは、CI環境のコンテナ起動時に、現在のネットワーク状況をプローブし、最適なレジストリミラーを環境変数経由で `.npmrc` に書き出す自動化スクリプトをパイプラインの先頭に配置する。
—
4. コンテナ環境におけるメモリと並列度の極致
CIでの `pnpm install` がOOM(Out of Memory)で落ちる場合、並列度の調整が不可欠だ。
並列実行数を制限する
デフォルトはCPUコア数に依存するが、メモリ制約のある軽量コンテナでは 2〜4 に固定する
export NPM_CONFIG_CONCURRENCY=2
pnpmの場合、さらに実行効率を最大化する設定
インストール後のリンキング処理を最適化
export PNPM_USE_CONTENT_ADDRESSABLE_STORAGE=true
また、Dockerコンテナ内では、`–frozen-lockfile`(pnpm)または `–ci`(npm)を必ず使用すること。これは、依存関係の解決をスキップし、ロックファイルの内容を忠実に再現させることで、計算リソースの消費を最小限に抑えるための必須要件である。
—
5. アーキテクトの視点:CI/CDパイプラインとの高度な連携
最後に、CIにおける究極の最適化として、「依存関係のキャッシュ戦略」を構築する。
GitHub Actions等を利用している場合、`~/.pnpm-store` をキャッシュするだけでは不十分だ。ロックファイルが更新された瞬間にキャッシュが汚染されるリスクがあるため、以下のアーキテクチャを推奨する。
1. コンテンツハッシュによるキャッシュキー管理: `package-lock.json` または `pnpm-lock.yaml` のハッシュ値をキャッシュキーに含める。
2. Post-Installの検証: インストール完了後に `pnpm audit` や `pnpm list` で依存関係の整合性を確認し、失敗した場合はキャッシュを破棄(Purge)するフローを組む。
3. レジストリ・プロキシの導入: 企業のCI環境であれば、`npm` の全トラフィックをオンプレミス内のキャッシュサーバー(ArtifactoryやVerdaccio)経由にする。これにより、インターネットへの出口トラフィックを制御し、パケットロスを物理的に最小化できる。
結びに代えて
「ネットワーク障害でビルドが落ちる」という事象は、単なるインフラの問題ではなく、「信頼性(Reliability)をアーキテクチャに組み込めていないことの証左」である。
リトライ戦略、タイムアウトの調整、そして冗長化。これらを組み込むことで、CIパイプラインは「待たされる場所」から「信頼できる自動化の歯車」へと進化する。現場のエンジニアがインフラの揺らぎを意識することなく、コードに集中できる環境こそが、我々アーキテクトが提供すべき究極の成果物である。
さあ、今すぐ `.npmrc` を見直し、不安定なネットワークを「前提」とした強靭なパイプラインを設計してほしい。