KARTEの分析システムのNode.jsを16から20にアップデートしました

こんにちは。エンジニアのnaoyashigaです。

今回KARTEの分析システムにおいてNode.jsのアップデートを行いました。

背景

KARTEの分析システムではフロントエンドとバックエンドの開発にNode.js / npmを使っています。

今回以下のようにアップデートを行いました。

Node.js 16のLTS(長期サポート)は2023年9月で終了しており、このまま使い続けるとNode.js公式や開発者コミュニティのサポートが受けられないこと、npmパッケージの互換性を保つのが難しいという理由からアップデートする必要がありました。

また機能面でも直近でWeb Stream APIを使いたいという要望がありました。これはNode.js18からexperimentalな機能として導入されたAPIです。Web Stream APIが使えるとAI関連のサービス開発においてストリーミング返信が使えユーザー体験が向上します。社内でもAI開発需要が高まっていたのでNode.jsのアップデートの優先度が高くなりました。

他にもfetch APIが使えるようになったこと(Node.js 18からexperimentalな機能で公開)、Test runnerが同梱されるようになったこと(Node.js 20から公開)という利点もありました。

アップデートするバージョンは20にしました。Node.js 20のLTSは2024年10月までサポートされています。2024年に切れてしまいますが16から22へのアップデートは変更量がさらに多いことが予想されるため20にしました。

アップデート後のマイナー、パッチバージョンの決め方

弊社にはdocker-compose.yml, dockerfile にある Node.js の version を自動アップデートするスクリプトがあり、それをGithub Actionsと連携して自動でPRを作る仕組みがあります。

バージョンを決めるロジックは以下のようになります。

  • Github Actionsを実行した時点でのNode.jsのLTSバージョンのうち最新のマイナー・パッチバージョンをアップデート後のバージョンとする
  • バージョンの情報は https://nodejs.org/dist/index.json をパース

この仕組みに従うとアップデート作業を開始した2024年4月頃の時点でNode.jsのLTSバージョンのうち最新のバージョンが20.12.2でした。またアップデート後のnpmのバージョンはNode.js20.12.2が推奨する10.5.0としました。

アップデートの進め方

段階的なアップデートを行う

今回アップデート対象となる分析システムは一つのレポジトリ内でコードを管理しています。アップデート対象となるpackage.jsonの数は21ありました。最初は一度のリリースで全てアップデートしようと試みましたが、変更点が多く動作確認にコストがかかることを予期して早々にこの選択は止めました。そして対象を切り分けて段階的にアップデートすることにしました。

前述のWeb Stream APIの要望があったことから、まずはWebサーバを担うバックエンドをアップデート。その後にフロントエンドやバックエンドの一部のジョブを含むコードを2回に分けてアップデートするようにしました。

フロントエンドのコード量がバックエンドよりも多かったため、結果として早い段階でバックエンドのNode.js / npmをアップデートすることができました。

packge-lock.jsonのlockfileVersionの詳細

今回npmをアップデートする上でpackage-lock.jsonの書き方が変わります。

https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#lockfileversion

lockfileVersionは現在1,2,3があります。それぞれの関係は以下のようになっています。

  • バージョン1
    • npm v5, v6で使われています
  • バージョン2
    • npm v7, v8で使われています
    • バージョン1と下位互換性があります
  • バージョン3
    • npm v9以上で使われています。
    • npm v7と下位互換性があります

今回はnpm v6.14.16v10.5.0 にアップデートするのでlockfileVersionはバージョン1から3に変わります。互換性はありませんので内容が変わります。なのでアップデート作業時はpackage-lock.jsonを作成し直す必要があります。

Node.jsのアップデート内容を確認

Node.jsの各バージョンの変更点を公式ページで事前に確認しておきます

後述のアップデート工程を経てわかったことですが、Node.js起因で問題になることはほとんどありませんでした。アップデート時に起こる問題のほとんどはnpmとnpmパッケージに関することが大半でした

npm installを行う

前述の自動アップデートするスクリプトをGitHub Actionsで実行しdockerのimageをNode.js 20.12.2にアップデートしておきます。

{
	..., // 途中省略
  "packageManager": "npm@10.5.0"
}

次にpackage.jsonにあるpackageManagerのnpmバージョンを10.5.0に書き換えます。弊社ではアップデート前からcorepackを導入済みでしたのでpackage.jsonにpackageManagerフィールドの記述がありました。corepackを初めて導入する場合はpackageManagerフィールドに新しいnpmのバージョンを記載してください。

次はnpm install工程です。ローカル開発環境でインストールを行う場合は、インストール前にnode_modulesフォルダが存在する場合は削除してください。前述のpackage-lock.jsonのlockfileVersionが1から3に変わることから、package-lock.jsonを新規作成する必要があるためです。npmではnode_modulesフォルダが存在する状態かつpackage.jsonの依存関係が更新されていないと、npm installをするときにpackage-lock.jsonを更新しない場合があります。したがってnode_modulesを削除してnpm installすると良いです。

またローカル環境でインストールを行うときは、当然ですがローカル環境のNode.js, npmを指定のものにする必要があります。弊社ではnpm installをmakeコマンド経由で実行することが多いです。Makefileでcheck-node-versionを使うことで推奨バージョン指定を提示しました。

sample-install:
	npx check-node-version --node 20 --npm '>= 9'

https://www.npmjs.com/package/check-node-version

具体的にはpackage-lock.jsonのlockfileVersion3に対応するためにnpm 9以上が必要としました。Node.jsは20を指定しました。マイナー・パッチバージョンについては個々のローカル作業環境で差異が生じる場合があり、変更コストがかかるので強制はしていません。

check-node-versionを使う前はアップデート後にローカルのNode.js, npmのバージョンアップ推奨の周知を入念に行ったり、エンジニア個々からの問い合わせに対応する必要がありました。今ではローカルでのバージョンが条件を満たしていないとエラー文が表示されるので問い合わせが減りました。

npm install後のエラーに対しての修正方法

peerDependencies起因のエラー文を読む

npm installすると大量のエラーが出ます。主な原因はpeerDependenciesで指定されたバージョンと互換性がない場合です。npm6まではpeerDependenciesは自動的にインストールされませんでしたがnpm7以降はデフォルトで自動インストールされます。(公式ドキュメント

具体的なエラー文は「npm package Aを使うためにはnpm package Bをversion X.X.X以上にしてください」というようなものです。このエラー文に従いnpmパッケージをバージョンアップします。

未使用コードの調査

次にやることはnpm package AやBが本当に必要かどうかの調査です。長年運用しているシステムではコードは存在しているが使われていないケースがあります。このケースを調査するためにnpmパッケージ名でgrepして実装を確認します。実際にこれを行った結果、使われていないコードがいくつかありました。またpackage.jsonにはパッケージ名が記載されているがコードでは使われていないというケースもありました。以上のような場合はパッケージは不要なのでpackage.jsonから記述を削除して再びnpm installします。

アップデート完了後に知ったのですがJS / Typescriptの未使用コードの検知は「knip」というツールを使うこともできます。次回アップデートからは積極的に使用したいと思いました。

アップデート推奨npmパッケージの調査

npm.png
(npmパッケージの例 : https://www.npmjs.com/package/vue )

未使用コードの調査ができたら次はアップデート推奨npmパッケージの調査です。npmの対象パッケージのページを確認します。そこでLast Publishの日付を見て継続されてメンテナンスされているパッケージなのかを判断します。またversionsタブから過去にリリースされたバージョンリストを確認し、現在使用しているバージョンがどれくらい古いものなのか、推奨バージョンはいつ頃リリースされたものなのかを確認します。

  • 継続されてメンテナンスされていない場合

まず対象パッケージが既存コードでどれくらい利用されているかを確認します。次にメンテナンスがされている代替パッケージを調査します。調査ができたら既存のコードとの置き換えコストがどれくらいかを確認して置き換えるか、推奨バージョンにまで上げるだけにしておくかを判断します。パッケージによっては公式が推奨する代替パッケージを提示してくれることがあります。このような場合は移行がスムーズなケースが多いです。

  • 継続されてメンテナンスされている場合

アップデート推奨バージョンにアップデートします。これで一応npm install時のエラーは解消できるはずです。次に推奨バージョンより上のバージョンにアップデートできるかを調査します。npmパッケージの公式githubページを見るとrelease noteを確認することができます。そこで破壊的な変更がないバージョン かつ できるだけ新しいバージョンが何かを調べます。そしてそのバージョンにpackage.jsonを書き換えてnpm installしてみます。新しい依存関係のエラーが出たらバージョンを古いものにしてnpm installします。これを繰り返すことで少しでもパッケージを新しいものにすることができます。

コード修正

npm installのエラーが解決できたらアップデートしたnpmパッケージに関連するコードを修正してテストを通していきます。根気がいる工程ですが焦らずコツコツ修正すればいずれ終わります!

事前に知っておきたかったこと〜npm編〜

npm関連で事前に知っておきたかったことを書きます。

deprecatedされたコマンドオプションがある

  • -unsafe-perm
    • npm7で廃止されました
    • https://github.com/npm/feedback/discussions/121

circle CIのジョブ内でnodeのバージョンを統一する必要がある

  • 事象
    • CircleCI内のジョブで異なるNode.js環境を前提としてnpm ciを実行するとsegmentation faultが発生しました
  • 原因の可能性
    • Node.js 16環境で、Node.js 20で生成したnpmキャッシュを使用すると問題が発生
    • Node.js 20環境(npm10)で作成したpackage-lock.jsonを使用してNode.js 16環境で npm ciを実行すると問題が発生
  • 解決策
    • 同じジョブ内で異なるNode.js環境を扱わないようにします。すべての環境をNode.js 20に統一しましょう

webpackのビルド時の使用メモリ量が増加する場合がある

  • 原因の可能性
    • npmパッケージをバージョンアップしたことにより使用メモリ量が増加した可能性があります
  • 対策

不正なpackage-lock.jsonが作成される時の対処法

  • 事象
    • 不正なpackage-lock.json(lockfileVersion3)が作成されることがあります
  • 作業工程
    • npm6でpackage-lock.jsonが作成される(lockfileVersion1) → npm10にしてnode_modulesを削除 → npm install → lockfileVersion3のpackage-lock.jsonが作成される
  • 対処法
    • node_modulesとpackage-lock.jsonも一緒に削除してください。その後  npm install をすると解決できました

事前に知っておきたかったこと〜corepack編〜

corepack v0.20.0からコマンドがupdateされた

  • https://github.com/nodejs/corepack
  • corepack v0.20.0でコマンドが刷新されました
  • v0.20.0ではcorepack prepareが廃止されています
    • グローバル環境にnpmをインストールする場合は corepack install -g npm@10.5.0 を使用します
    • package.jsonのpackageManagerに明示的にnpmのバージョンを指定していない場合、global環境のnpmを使うのでその時のnpmバージョンを明示的にinstallしておけます

環境変数:COREPACK_ENABLE_DOWNLOAD_PROMPT

  • corepack仕様
    • corepackにはCOREPACK_ENABLE_DOWNLOAD_PROMPTという環境変数がありこれを0に設定すると「ダウンロード質問」は表示されません。CI環境では基本的にダウンロード質問は表示されません。しかしsudo実行など環境変数が引き継がれない操作を行った時はCOREPACK_ENABLE_DOWNLOAD_PROMPT の設定が必要となります。
  • corepackの実装

事前に知っておきたかったこと〜その他〜

古いnpmパッケージがNode.js 20に非対応 かつ npm install時に気づけないケース

  • npm install時にエラーに気づけないケースがありました
    • 以下の条件をすべて満たす時、気づけませんでした
      • package-lock.jsonnode > 0.10.0のように下限のバージョン指定がありこの条件は満たしていました
      • バージョン上限の指定がありませんでした
      • npmパッケージの古いバージョンがリリースされた当時、Node.js 20がまだ存在していませんでした
  • 起こったこと
    • npmパッケージの古いバージョンにおいてNode.js20での動作検証が行われていないので実行時に不具合が発生しました
  • 対策
    • 事前に気づくことは難しいですがパッケージのchangelogを確認しNode.jsの各バージョンに対する対応状況を把握しておくと良いです

デプロイ時の動作確認方法

npmパッケージの更新をしてunitテスト、e2eテストを終えたらデプロイを行います。KARTEでは検証環境と本番環境があります。今回は段階的にアップデートしているものの影響範囲が多かったので複数人での手動での画面動作確認を入念に行いました。

前回のNode.js16のアップデート時に動作確認チェックリストをnotionのテンプレートにしていたのでそれを継続して使用しました。notionで確認項目をタスクに分けて担当者に分けることでスムーズに動作確認を行うことができました。

検証環境で動作確認をして不具合が起こった場合はrevertして不具合対応を行います。実際にいくつか不具合が見つかったので、複数人での画面確認はコストがかかりますがやって良かったと思います。

最後に

本記事では、KARTE分析システムにおけるNode.jsおよびnpm, npmパッケージのアップデート工程について紹介しました。多くの方々のサポートのおかげアップデートを行うことができました。改めて感謝申し上げます。また本記事がNode.js, npmアップデートを今後する方の参考になれば幸いです。