フロントエンドのライブラリ134個を一気にアップデートしてリリースした話
こんにちは、アルダグラムの田中です。
今回はフロントエンドで利用している npm package
を合計134個、最新バージョンへ更新し、最終的に不具合を発生させず一括リリースできた話について紹介します。
一例として以下のライブラリのバージョンアップを行いました。
before | after | |
---|---|---|
Next.js | 12.2.0 | 15.0.3 |
React | 17.0.2 | 18.3.0 |
Node.js | 16.14.2 | 22.11.0 |
@apollo/client | 3.3.6 | 3.11.10 |
Storybook | 6.5.10 | 8.4.7 |
ESlint | 8.22.0 | 9.16.0 |
TypeScript | 4.6.2 | 5.7.2 |
また134個のライブラリをバージョンアップしたことにより、以下の成果も得られました。
-
npm audit
で検出される脆弱性を95%削減 - ページの表示速度を1.5秒改善
- 発生していたエラーを80%解消
最近ライブラリアップデートできてないなーという方、今後ライブラリアップデートを検討している方、定期的にライブラリアップデートをしているぞという方も、ご一読いただけると幸いです。
※なお、本記事では個々のライブラリのアップデート手順には詳細に触れませんのでご了承ください。
まず結論
ライブラリを一括アップデートするのは全くもってお勧めしません。
こまめに上げることをお勧めします。
地獄を見ます。
はじめに
普段はSREを担当していますが、KANNAの初期開発に携わっていたこともあり、「フロントエンドのライブラリのアップデートをやってほしい」という依頼を受けました。
当時、フロントエンドのライブラリに関して以下の課題がありました。
- ライブラリの更新が長期間行われておらず、技術的負債が蓄積
-
npm package
に潜在的な脆弱性が多く残存 - 古いバージョンゆえに最新機能が活用できない
これらの課題をまとめて解消すべく、ライブラリを最新化するプロジェクトをスタートしました。
ライブラリアップデートの方針
「ライブラリアップデート」と一口に言っても、どこを目標としたかという話ですが、
主要なライブラリを最新化する
ことを目標と置きました。
つまりこの時点での目標は、以下のライブラリを最新化することを目標としていました。
before | after | |
---|---|---|
Next.js | 12.2.0 | 14.2.4 |
React | 17.0.2 | 18.3.0 |
Node.js | 16.14.2 | 18.x.x |
このアップデートは開発組織全体に影響を及ぼすため、QAスケジュールやリリーススケジュールをあらかじめ調整していました。
またアルダグラムではユニットテストやE2E、VRTなどを整備してはいるものの、フロントエンドの根幹となるライブラリをアップデートするため、全体的にQAした方が良いのでは?という考えに至り、QAチームとも連携し、アップデートに関する品質保証体制を固めました。
なぜ一気に上げることなったのか?
嬉しいことに目標としていたライブラリのアップデートが想定よりも早く終わりました。
そのため全体QAまでに一定の期間が開くことになりました。
(さてこの期間で何をするか・・・)
(通常のSRE業務に戻ろうか・・・)
(なんか他のライブラリも最新版バージョンと乖離があるものがかなり多いな・・・)
(この後、全体のQAを行うし、他のライブラリもバージョンアップして技術的負債を一気に解消しようかな・・・)
(・・・よし、他のライブラリもなるべくバージョンアップしてしまおう!)
「ついでにやってしまおう」くらいの感覚で意思決定をしてしまいました。
この「ついでにやってしまおう」という決断は、結果的に多くの不具合発生要因を内包することになりました。
ライブラリアップデートの具体的なやり方
具体的にどのような方法でライブラリをアップデートしたかについてご紹介します。
npm outdated
でアップデート対象を確認
1. npm outdated
は、Node.js向けパッケージ管理ツールであるnpmにおけるコマンドの一つで、現在使用しているプロジェクト内の依存パッケージ(dependencies)のバージョン状況を確認し、利用可能な更新があるかどうかを一覧表示するために用いられます。
npm outdated
コマンドを実行すると、以下の情報がテーブル形式で表示されます。
$ npm outdated
Package Current Wanted Latest Location Depended by
@apollo/client 3.11.10 3.11.10 3.12.3 node_modules/@apollo/client kanna-web
@babel/cli 7.25.9 7.25.9 7.26.4 node_modules/@babel/cli kanna-web
...
- Package(パッケージ名)
- Current(現在インストールされているバージョン)
-
Wanted(
package.json
で指定されたバージョンレンジにおいて利用可能な最新のバージョン) -
Latest(
npm
リポジトリ上で入手可能な最新版のバージョン) - Location(インストール元となる階層、ディレクトリ)
どのパッケージが古くなっているか、最新のバージョンが存在するかなどを一目で把握できます。
npm outdated
の結果をNotionに転記
2. 各ライブラリのアップデート状況や対応Pull Request、優先度(結果的にアップデートできるものは全てあげたのですが)、その他留意事項などを管理したかったため、Notionにnpm outdated
の結果を転記しました。
ユニークIDプロパティをブランチ名にすることで、アップデート対象のライブラリとGithubのPull RequestをNotion上で連携することができるのが便利です。
3. 変更差分を確認する
typescript-eslint
を例に挙げます。
アップデート対象のライブラリを https://www.npmjs.com/
で検索します。
※お使いのエディタによっては、エディタ上から直接対象のページに遷移することもできます。
次に現在のバージョンとアップデートするバージョンにどのような変更が入っているか確認しましょう。
どんなライブラリにも基本的には以下のいずれかにアップデート内容が記載されています。
- Githubのreleaseのページ
- CHANGELOG.md
-
ライブラリのホームページ
- ブログ
- マイグレーションガイド
- アップグレードガイト
特にBREAKING CHANGE(破壊的変更)
をチェックしましょう。メジャーアップデートの際にBREAKING CHANGE
が発生するケースが多いです。
4. ライブラリをアップデートする
以下のコマンドを実行し、ライブラリをアップデートしましょう
$ npm install --save-exact --save-dev typescript-eslint@latest
各オプションの意味について簡単に説明します。
--save-exact
はバージョン番号がそのまま固定された状態で記録され(例:"2.3.4"
)、npm install
実行時にその正確なバージョンのパッケージがインストールされることになります。
マイナーバージョンで大きな変更が加わるライブラリや、パッチで挙動が変わるライブラリも存在するので、基本的にバージョンは固定する方針にしています。
--save-dev
は開発環境用の依存関係(devDependencies
)として、package.json
にパッケージを登録します。typescript-eslint
はテストやリンティング、ビルドプロセスなど開発時にのみ必要なツールであり、実行環境(本番環境)では不要なため、devDependencies
に追加するのが一般的です。
5. 影響があった箇所を修正する
先に確認したBREAKING CHANGE
の修正や変更差分の影響を確認し、各種Lint、型チェック、VRT、ユニットテスト、インタラクションテストなどで問題がないかを確認します。
6. Pull Requestを出す
CIがOKだったらPull Requestを出し、レビューしてもらいましょう。
私の場合は、対象の変更差分へのリンクと具体的なコードの変更内容を記載しました。
レビューがOKであればマージしましょう。
最終的に
アップデートに際し、数多くの障壁がありましたがなんだかんだで全てアップデート仕切りました。
いざQA
さてCIも全て通り、全体QA(手動テスト)のフェーズです。
CI上ではLinter ・typeチェック・VRT・インタラクションテスト・ユニットテストといったテストはすべで合格となっていました。
不具合は出るだろうけど、そこまで深刻な不具合は出ないのでは?と考えていました。
しかし、その考えは甘かったです。
実際に全体QAを行なってどうだったかというと大惨敗、致命的な不具合が70件ほど生じました。
ここまで多くのライブラリを一気に上げると何が起因で起きた不具合かよくわからないものが多数ありました。
例えば
- ページに謎のボタンが出現する
- URLに直アクセスした際にページが2重にレンダリングされる
- 元々論理的に間違っていた処理が不具合として顕在化した
などなど
余談ですがあまりにも奇想天外な不具合が多かったので、思わずまとめ記事を作り全体に共有してしまいました。
元々リリース予定としていた日に修正が間に合わないため、リリースは2ヶ月後に延期となりました。
なぜ失敗したのか
一番失敗だと感じたことは全体QAの前にE2Eテストを実行できていなかったことです。
ライブラリ対応毎にE2Eを実行していれば、何が起因で起きた不具合か検知しやすかったと感じています。
※苦しい言い訳ですが社内事情により、E2E実行環境がメンテナンス中であったため実行は難しかったという経緯もあります。
再リリース日までにやったこと
さてここからが大変です。
何せライブラリアップデートの統合ブランチとリリースブランチは別なのですから。
特に苦労した点は以下の通りです。
-
リリース毎に大量の差分を統合ブランチに取り込むこと
コンフリクトが大量に発生します。実際に機能開発には携わっていないので、もはや雰囲気でコンフリクトを解消しています。デグレが発生しないようにリリース毎のテスト計画書を参考に手動テストを実施しました。 -
再リリース日までにライブラリを最新バージョンに追従すること
ライブラリは日々最新化されています。これらをなるべく最新化するためにアップデートを随時行いました。最新化するたびにまたコンフリクトが発生します。しかし結果として、Next.js
は14系ではなく15系に上げることができました。
前回の反省を生かし、E2Eテストを定期的に実行し、見つけた不具合は事前に修正しておきました。
いざ再QA
再び手動によるテストを実行しました。
事前にE2Eテストを実行していたため、致命的な不具合はなかったものの、20件ほどの不具合を検知しました。
機能仕様が曖昧な点は他チームの協力いただきながら、不具合を修正しました。
また、本番に近いステージング環境での検証でも問題がなかったため、最終的にリリースへ踏み切りました。
そしてリリース
最終的な差分はというと
こうなりました。
これをリリースするのか・・・怖い・・・怖すぎる・・・。
リリース中は
-
Datadog
の各種ログやモニタリング用のダッシュボードの監視 - ユーザーからのお問い合わせメールなどを監視
を行いながら作業をしました。
結果、不具合なくリリースすることができました。
また万が一に備えて、デプロイをロールバックできる時間を延長しました。
現時点においてもライブラリアップデートによる不具合は確認されていません。
最後に
今回の経験から得た教訓は、やはり 「ライブラリは段階的かつ継続的にアップデートすること」 の重要性です。
一括アップデートは、
- 不具合や潜在的問題が同時多発し、原因特定(トリアージ)が困難になる
- QAコストが増大する
- リリーススケジュールが不確定になりやすい
といった多大なコストを招きかねません。
現在では、フロントエンドにRenovate
を導入し、定期的かつ小規模な単位で継続的なアップデートを運用しています。
同様の課題に直面するエンジニアの皆様にとって、本記事が参考になれば幸いです。
※1 本件のライブラリアップデートは、着手から最終リリースまで約半年を要しました。
※2 不具合なくリリースできたのは、QAエンジニアの皆様の多大なご尽力によるものです。
※3 当初、フロントエンド以外(アプリやサーバーサイド)では既にRenovate
が導入済みでしたが、フロントエンドは未整備の状態でした。
株式会社アルダグラムのTech Blogです。 世界中のノンデスクワーク業界における現場の生産性アップを実現する現場DXサービス「KANNA」を開発しています。 採用情報はこちら: herp.careers/v1/aldagram0508/
Discussion