ブログ

パッケージ管理を再考する:なぜ今npmからpnpmへの移行が推奨されるのか

You Should Move to pnpm from npm Now という記事を読み、パッケージマネージャーの選択が単なる開発効率の向上だけでなく、セキュリティ対策として大きな意味を持つことを改めて考えさせられました。今回は、実務の視点からpnpmへ移行すべき理由を整理してみたいと思います。

昨今のセキュリティ対策を考えると移行するべきか…。


私たちがnpmを使い続ける理由と、その背景にある「慣性」

多くのプロジェクトでnpmが標準的に使われているのは、性能面で優れているからというよりは、むしろ「慣性」に近いものがあるかもしれません。Node.jsをインストールすれば最初から入っており、ドキュメントも豊富で、チームに新しく入ったメンバーもそのまま使いこなせます。

「今のところ動いているから変える必要がない」という判断は、開発現場では一般的かと思います。しかし、サプライチェーン攻撃が巧妙化している現状において、この「当たり前」を疑う時期が来ているのかもしれません。

npmとpnpmの主な違い

まずは、両者の特徴を簡単に比較してみます。

項目 npm pnpm
ストレージ効率 各プロジェクトに実体をコピー(重複が多い) コンテンツアドレス指定ストアによる共有(効率的)
インストール速度 比較的遅い(特に大規模プロジェクト) 高速(キャッシュとハードリンクを活用)
node_modules 構造 フラット(依存関係が公開されがち) 厳格(依存関係の分離が徹底されている)
セキュリティ postinstall などのスクリプト制御が緩やか 実行制限やセキュリティ機能が強化されている

深刻化するサプライチェーン攻撃の脅威

npmを使っている際に注意が必要なのは、パッケージの「インストール時」に実行されるスクリプトの脆弱性です。

例えば、ある人気パッケージのメンテナーのトークンが流出し、悪意のあるアップデートが公開されたとします。多くのプロジェクトでは、セマンティックバージョニング(^1.2.0 など)によって自動的に最新のパッチを取り込む設定になっているかと思います。この際、悪意のあるコードが含まれた postinstall スクリプトが実行されると、CI/CD環境から秘密情報(環境変数など)が外部へ送信されてしまう恐れがあります。

以下に、典型的な攻撃の流れを可視化してみました。

sequenceDiagram
    participant Attacker as 攻撃者
    participant Registry as npmレジストリ
    participant CI as CI/CDパイプライン
    participant Secrets as 環境変数・秘密情報

    Attacker->>Registry: 悪意のあるパッチ (v1.2.1) を公開
    CI->>Registry: npm install (自動アップデート)
    Registry-->>CI: 悪意のあるパッケージをダウンロード
    CI->>CI: postinstall スクリプトが実行される
    CI->>Secrets: 秘密情報をスキャン
    CI->>Attacker: 盗み出した情報を送信

実際に、npmやPyPIなどの複数のエコシステムを標的とした攻撃キャンペーンも報告されており、単に「有名なパッケージを使っているから安心」と言い切れないのが今の実情ではないでしょうか。


pnpmが提供する「防御」の仕組み

pnpmが単なる「速いnpm」ではないと言われる理由は、そのアーキテクチャにあります。

コンテンツアドレス指定ストア

pnpmはパッケージの各バージョンを、ディスク上の1箇所(コンテンツアドレス指定ストア)にのみ保存します。各プロジェクトの node_modules には、そこへのハードリンクが作成される仕組みです。

これは「図書館の蔵書システム」のようなイメージに近いです。 各家庭(プロジェクト)で同じ本(パッケージ)を何冊も買い揃えるのではなく、中央の大きな図書館(ストア)にある本を、自分の家の窓(リンク)から覗き見ているような形です。これにより、ディスク容量が節約されるだけでなく、パッケージの実体が不用意に改ざんされるリスクも抑えられます。

厳格な依存関係の管理

npmでは node_modules がフラットに展開されるため、自分が直接依存していないパッケージ(依存の依存)にもアクセスできてしまう「幽霊依存(Phantom Dependencies)」が発生しがちです。一方、pnpmはシンボリックリンクを駆使して、package.json に記述されたパッケージ以外にはアクセスできないような構造を強制します。

flowchart TD
    subgraph pnpm_structure [pnpmの構造]
        A[Project Root] --> B[node_modules]
        B --> C[.pnpm store]
        B --> D[Direct Dependency A]
        D -.-> C
    end
    style A fill:#f9f,stroke:#333,stroke-width:2px

このような厳格な管理によって、意図しないパッケージの実行や、依存関係の隙間を突いた攻撃を防ぎやすくなります。


まとめ:移行へのステップ

pnpmへの移行は、思っているよりもスムーズにいくことが多いです。

  1. pnpmのインストール: corepack enablenpm install -g pnpm で導入できます。
  2. ロックファイルの変換: pnpm import を実行すれば、既存の package-lock.json から pnpm-lock.yaml を生成できます。
  3. CI/CDの更新: npm installpnpm install に書き換えます。

もちろん、一部の複雑なビルドツールでパスの解決に問題が出る可能性もありますが、そうしたケースも最近では少なくなってきている印象です。

「速いから変える」という理由も魅力的ですが、これからの時代は「プロジェクトの安全性を一段階上げるため」にpnpmを選択するというのが、実務家らしい判断と言えるかもしれません。まずは個人のプロジェクトや、影響の少ない内部ツールから試してみてはいかがでしょうか。

参照記事