Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Eight の Node.js を 16 から 20 にアップデートしました

こんにちは。 Eight でエンジニアをしている鳥山(@pvcresin)です。
今回は、Eight で使用している Node.js をアップデートした際の手順や、ハマったところについてお話ししたいと思います。

目次

背景

Eight では Web フロントエンドの開発やビルドに Node.js を使っています。 使用していた Node.js 16 のサポートが 2023-09-11 に終了するということで、アップデート対応をそれまでに行う必要がありました。
当初、Node.js 16 は 来年(2024 年)の 4 月までサポートされる予定でしたが、内部の OpenSSL 1.1.1 のサポート終了日と合わせるため、後からサポート期間が 7 ヶ月短縮されたという経緯があります 😵
また、リリーススケジュールでは、その約 1 ヶ月後の 2023-10-18 に Node.js 18 がメンテナンスモードに入り、その 6 日後の 2023-10-24 からは 20 が Active LTS になることが決まっていました。 そういった状況を踏まえ、Node.js を 16 から一気に 20 まで上げてしまうことにしました。

手順

依存が少ないパッケージから対応し、動作環境はローカル → CI/CD の順に確認するという方針のもと、以下のような順で対応を行いました。

  1. 社内向けライブラリ群
  2. ライブラリを利用している各サービスのコード
  3. 一部の CI で使っている 自作の Docker イメージ
  4. ビルドで使っている CodeBuild の設定
  5. ドキュメント更新

1. 社内向けライブラリ群

Eight には共通コンポーネントや通信に関するものなど、フロントエンド開発に使う社内向けライブラリが複数存在しています。 まずはバージョン管理ツールを使ってローカルの Node.js のバージョンを 16 から 20 に変更し、各ライブラリが問題なく動くかどうか確認していきました。
package.json にある npm script を地道に一つずつ実行していき、正常に動かない場合があればコードの修正や依存ライブラリの載せ替えなどを行います。 package.json の engines も更新します。

"engines": {
  "node": ">=20"
}

ローカルで完璧に動くことが確認できた後は、CI/CD 環境のチェックです。 CI には Circle CI や GitHub Actions を使っていますが、社内向けライブラリではそれぞれの公式が用意している Node.js 環境を使っていたので、設定ファイルの Node.js のバージョンを上げるだけで対応は終わりました。 最後に対応済みのバージョンをリリースして完了です。

2. ライブラリを利用している各サービスのコード

次に、社内向けライブラリを利用している各サービスのコードです。 Node.js のバージョンの他、さきほど対応したライブラリ群のバージョンも変更して動作チェックを行っていきます。 やることはさっきとほぼ同じです。 サービスのコードなので、入念に動作確認を行いました。

3. 一部の CI で使っている自作の Docker イメージ

一部のサービスの CI では、最適化のために自作の Docker イメージを使っています。 この自作のイメージには Node.js のセットアップも含まれていたので、Docker 公式の Node.js イメージを参考にしつつ、Node.js 20 に対応した Dockerfile に変更しました。 イメージタグを更新した上で Amazon ECR に Docker イメージを push することで CI で使えるようになりました。

4. ビルドで使っている CodeBuild の設定

次に CD 周りを見ていきます。 と言っても、手を加えたのはデプロイの前のビルドの設定のみです。 フロントエンドのビルドには AWS CodeBuild を使っているため、そこで Node.js 20 が使われるように設定ファイルを変更しました。 検証環境へのデプロイが成功したら、mabl で E2E テストを実行したり、手動で動作確認を行いました。

5. ドキュメント更新

最後に忘れてはならないのが、ドキュメントの更新です。 リポジトリ内や Notion などのドキュメントを node といった単語でざっくり検索して Node.js のバージョンに関する記述を更新しました。 これで対応は完了です 🎉

ハマったところ

最後に、アップデート作業を行う中でハマったところを 3 つ紹介します。

ライブラリが Node.js の Fetch API に対応できてない

社内向けの通信系ライブラリで、ユニットテストが通らないという問題がありました。 これは Node.js 18 からフラグ無しで実行可能になった Fetch API に、Nock という通信をモックするライブラリが対応できていないことが原因でした。
この Nock の問題は以前から指摘されていたようなので、対応される望みは薄そうです。 テスト部分しか問題がなかったので、ひとまず --no-experimental-fetch をつけることで回避しました。

"scripts": {
  "test": "NODE_OPTIONS='--no-experimental-fetch' jest"
}

ただし、この対応は一時しのぎであり、ずっとこれで誤魔化すわけにもいきません。
幸い Nock を使っている部分がそこまで多くなかったこともあり、Fetch API に対応していて汎用性が高い MSW(Mock Service Worker)に、後で載せ替えることができました。1

// Nock
nock(baseUrl).get("/user").reply(200, { id: 1, name: "John" });

// MSW
rest.get(`${baseUrl}/user`, (req, res, ctx) => {
  return res(ctx.status(200), ctx.json({ id: 1, name: "John" }));
});

jsdom と Node.js で Blob や File がぶつかる

ブラウザで動く関数は、Node.js 上にブラウザ関連の API を用意する jsdom というライブラリを使ってテストしています。 それらのうち、FileReader を使ってファイル読み込みを行う関数など、File 関連のテストが通らないという問題がありました。 これは、Node.js 18 から Blob が、20 から File が、それぞれグローバルオブジェクトになり、一部のコードで jsdom の Blob や File として扱われなくなってしまったことが原因でした。
これに関しては、テストコードの修正が難しかったため、テストの setup 時に Node.js のグローバルの Blob や File を jsdom のもので上書く対応を行いました。

const { window } = new JSDOM('<!DOCTYPE html><html><body></body></html>');
globalThis.Blob = window.Blob;
globalThis.File = window.File;

CodeBuild で Node.js 20 に対応した公式イメージがない

Eight では CodeBuild に、AWS が提供する公式の Docker イメージを使っていますが、Node.js 20 に対応したイメージ2がまだ公開されていないという問題がありました。 どうやら AWS 側は LTS になってから対応を始める方針のようです。 今回は 20 に一気に上げてしまいたかったので、少し困りました。
回避策を探してみると、以下のように commands を使った方法が見つかりました。

# buildspec.yml
phases:
  install:
    runtime-versions:
      nodejs: 18
    commands: # 直接コマンドを呼び出す
      - n 20

実は CodeBuild の公式 Docker イメージの内部では、Node.js のバージョン管理ツールである n が使われており、それを利用することで外部から強制的に Node.js のバージョンを変更することができました。
ただし、これは回避策に過ぎないので、Node.js 20 に対応した AWS 公式のイメージが出た際には、乗せ替えようと思います。

まとめ

まずは、大きなトラブルなく Node.js を 16 から 20 にアップデートすることができて、ひと安心しています。
毎週持ち回りでライブラリアップデートなどをコツコツ行ったり、定期的に古いライブラリを置き換えたりしていたのが良かったのかなと思いました。
新しいバージョンが出るということは、それなりに嬉しいアップデートもあるはずなので、積極的に向き合っていきたいですね。

© Sansan, Inc.