こんにちは。ソーシャル経済メディア「NewsPicks」NewsPicks Stage.事業のエンジニアをしています、林です。 業務では Next.js / Rust / Go などを用いて、経済・ビジネス情報に特化した動画配信サービスであるNewsPicks Stage.の開発・運用を行っています。
はじめに
突然ですが、皆さんは自身のソフトウェアのライブラリアップデートは行えていますか?
皆さんはどのようにライブラリアップデートを行なっていますか?
新機能を試したくて? npm i
で失敗してから頑張る? Renovate / dependabot が自動Mergeされる環境? もしくは対応担当が特定の日にまとめてMergeする運用?
しかし多くの開発者は、アップデートに対して「うまくいっている」と言えないのではないでしょうか?自身も様々なプロダクトを開発してきた経験上、日々の中ではどうにもならなくなってしまいアップデートのプロジェクトが立ち上がったようなプロダクトや、EOL対応で焦って取り組むケースなどが見られ、難しさを感じていました。
以下の画像は、2024年1月1日から3月8日の期間に、NewsPicks Stage.のモノレポに対して行った私のライブラリアップデートのCommitを数えたものです。このデータを集めるためには、gitqlを使用しました。アップデートCommitには、:up: の絵文字を使ってマークしています。
バックエンドでもフロントエンドでも、大小さまざまな変更が含まれていますが、これらはほぼ全て本番環境にデプロイされ、問題なく動作しています。 1日に1回以上のペースでアップデートを行う中で、このプロセスを何が支え、どのようにして誰もが再現できるのかと考え整理してみたので、この記事を書くことにしました。
※前提として、WebアプリケーションのnpmパッケージやCargo crateといったソフトウェアライブラリのアップデートを想定しています。
ライブラリアップデートが必要な理由
まず、なぜライブラリアップデートが必要なのでしょうか?
ライブラリをアップデートする理由は、単に新機能を利用するためだけではありません。 新しいVersionのライブラリには、以下のような変更も含まれるからです。
- パフォーマンスの向上
- 開発効率の向上
- セキュリティの強化
パフォーマンスや開発効率の向上に関しては、単に嬉しい・ユーザー体験が高まる だけでなく、複利的に効果を発揮します。
セキュリティアップデートを怠ることは、攻撃者にとっての入り口を開放することに等しく、重大なデータ侵害や損害を引き起こす可能性があります。
ビジネスに関わるプロダクトを開発するエンジニアにとって、ライブラリアップデートは競合優位性・プロダクトの信頼などに関わる重要な事柄なのです。
ライブラリアップデートに関する絶対的な事実
技術ブログには適さないような断言なのですが、ライブラリアップデートに関しては絶対的な事実があります。当たり前のようなことなのですが、改めて整理しておきます。
利用しているライブラリは、自分たちのソフトウェアを構成する一部である
- 私たちは関数やクラスという抽象化された形でその恩恵を受けているので意識しにくいですが、その先にもソースコードがあり、それらを含めて自分たちのソフトウェアを形作っています。Versionの変更はソースコードの変更と同義であり、Versionが離れていることは、あるブランチの変更をずっとMergeできていないようなものである と考えられます。
誰かが上げないと、(基本的には)上がらない
- 現代のソフトウェア開発においては、依存関係はLockファイルなどを用いて厳密に管理されています*1。Renovate / dependabotなどによる自動Mergeしている場合を除き、誰かが「アップデートしよう!」という行動を起こさない限りアップデートは行われません。
「新しいVersionがある」ということを認識する必要がある
- 上記のようにVersionは固定されていることから、現状のプロダクトが勝手に壊れるようなことは(基本)起きないため、新しいVersionが出たことに気づかない限りアップデートに取り組む機会すらないでしょう。
Major Versionをアップデートする難易度は、Minor Versionをアップデートする難易度より高い
- Major Versionのアップデートは、Minor Versionの変更より多く、破壊的変更も含まれるため、一般的に難易度が高くなります。Minor Version - Patch Versionにも同じことが言えます。
明日アップデートするより、今日アップデートする方が簡単
- ソフトウェアは日々変更されます。勢いのあるOSSであれば毎日のように新しいバージョンがリリースされています。先にブランチの例えをしましたが、利用するライブラリのブランチがあるとして、ソースコードの差分が少ないほど、簡単なのは納得できると思います。
要するに、なるべく小さい変更のリリースでも素早く気付き取り込むことで小さな労力で済ませられ、それには誰かが動き始める必要があるのです。
では、何があれば継続的なライブラリアップデートを行えるのでしょうか?
なにが継続的なアップデートを支えるか
この問いに対する回答はシンプルで、私が感じているのは「日常にしてしまう」ことです。
理想論のようにも思えるかもしれませんが、先にも述べた通り ライブラリのソースコードは自分たちの保守するソースコードと同等のものだ と捉えられているかが感覚を分けると思います。
ソースコードを変更し、テストを通過させ、ユーザーに価値を届け、フィードバックを得る。そのサイクルをなるべく短くし、常に変化に対応するソフトウェアを届ける。それらが、私たちソフトウェアエンジニアの仕事であり日常なはずです。それは、自分の利用しているライブラリにも同じことが言えるはずです。
では「日常にしてしまう」理想に近づくには何ができるでしょうか。
成功体験を得、理解する
まずはPatch Versionひとつのような、とにかく小さくても良いので成功体験を得ることが重要だと考えています。 これは特に自分の経験のない言語などで起こることですが、ライブラリをアップデートするまでの道のりを「謎の手順」でなくすことが必要です。
「壊れそうで怖い」のような心理的ハードルは、「壊れなかった」「壊れたけどCIで検知できた」という経験でなくせるものです。 実際にPatch Version一つの変更でユーザーに重大な影響を与えた経験があるでしょうか?型による検証、Testや検証環境で検知できないでしょうか?B/Gデプロイなど、安全にデプロイ・ロールバックできる仕組みがあればリスクにも備えられるのではないでしょうか?
少しのVersion変更で重大な影響がでるようなソフトウェアであれば、そのソフトウェアはそもそも変更に強くないのかもしれません。逆に、日頃からソースコードを変更しデプロイしている環境であれば、過剰に恐れる必要はないはずです。
気をつけなければいけないのは、リリースすることを忘れないことです。ここでのリリースとは、実際の顧客に届くということです。 無事動き、ユーザーが変わらずに価値を得たというフィードバックを得て初めて成功です。小さなバージョン変更の場合、ほとんどが大きな影響はないはずなので「なんだこんなものか」という手応えを感じられると思います。
最新Versionを検知する手段を知る
ライブラリアップデートは「最新Versionがある」ということを検知しなくては始まらない という話は先ほどした通りですが、それを支えるツールを知ることから始めましょう。
先ほどから出ている Renovate / Dependabot のような依存関係自動化ツールをはじめ、maven でデフォルトで利用できるversions-maven-plugin、npm-check-updates などのOSSもあります。 これらをCIで実行する、結果を通知するなどで「検知」のアンテナを張っておきます。
特におすすめなのは、Editorの拡張機能です。
私は業務でVSCodeでRustを書くことが多いのですが、cratesという拡張機能を用いて、Cargo.tomlをひらけばアップデートが検知できる 状態を作っています。
マウスオーバーすれば一覧もわかるようになっています。開発でソースコードを変更する前に更新があるかクイックに見て、変更したらCommitしてCIを流しておく。そういう流れを開発に組み込んでいます。
Versionアップデート時の変更の追い方を知る
変更内容を知れば、アップデートのハードルはぐんと下がります。たとえば、「Major Versionが変わったが、中を見てみるとかなり昔のランタイムVersionのサポートを切っただけだった」ケースなどです。
大抵のOSSでは、CHANGELOGを追うことで事足りることが多いです。Github Releaseに変更内容が含まれていることもあります。どれもなければ、ソースコードの変更を少し追ってみます。
これも成功体験を重ねていくことが重要です。
「変更の影響がでかい」「難易度が高い」を知ることに価値がある
理想は「日常にする」と述べましたが、それでも変更に追従できない日はやってきます。Webフレームワークなどそもそも強く依存しているものの破壊的変更や、ライブラリからサポートが外されてしまった、あるライブラリの対応が遅れているので他をアップデートできない などです。
ですがアップデート自体には失敗しても、「事実」は得られたことになります。例えば「このソフトウェアは〇〇ライブラリの3系にあげられなくなってしまった」という事実が、リリースの翌日に判明するか、3年後に判明するかではその後のリスクやコストを大きく左右するのは想像に難くないと思います。(直面する問題も修正されているかもしれませんが)
複雑な依存関係や、そもそもの破壊的変更の影響が大きい という事実も、まとまった作業時間で対応する という選択肢を取りやすくなりますし、それを早く知るためにも日常的にアップデートに取り組めると理想的だと思います。
「根拠のある自信」を強める
ソースコードに歴史があり規模の大きいコードベースになると、ライブラリアップデートの影響範囲は追えなくなってくるでしょう。 ただそうなっている場合、ライブラリアップデートだけでなく既存のソースコードを変更するのにも慎重さが求められるようになっているのではないでしょうか。
これには、「根拠のある自信」を強めるような投資が必要だと考えています。この言葉は、t_wada氏が発信されていた言葉で、私も大切にしている価値観です。
基本的に、テストコードが適切かどうかの判断基準は、開発者の自信が増えるかどうかなんですね。もっと言えば、根拠のある自信がつくかどうかです。「自分が書いてきたコードは、少なくとも自分が書いた通りに動いているという自信」および「これからどんな変更が入っても柔軟に対応できるという自信」が大事なんです。
テスト駆動開発のはじめの一歩|t_wadaさんに聞く1人で始める自動テストのコツと考え方
ライブラリアップデートという文脈でも、適切なテストやプロセスがあれば根拠のある自信が持てるのではないでしょうか。 ライブラリに依存しないUnitテストだけでは、ライブラリアップデートが安全である根拠は得にくいですが、実際のライブラリを動かした状態の Integrationテスト / E2Eテスト があれば、動く根拠(もしくはリリースできない根拠)を得られます。 静的型チェックやLintも根拠の一つになります。
それらを自動で動かすCI環境があれば、開発者がアップデートにかかりきりにもならず日常的にアップデートを行う基盤となるでしょう。
OSSをホストする
私がVersionアップデートについてハードルが下がったなと思えることの一つとして、実際にOSSをホストした経験があります。
私はWiremockのGraphql Extension、wiremock-graphql-extensionを作成・メンテしています。実際にホストする側をやってみると、Version変更について、この期間、このVersionUpでどういう変更が含まれるのかの感覚がなんとなく身につきました。
ライブラリの作成者も後方互換を考え、Testを行った確かな質のコードを届けているはずです。アップデートを行われる側に立つことで、新しいバージョンのリリースが「得体の知れないもの」ではなくなっていくのです。
まとめ
ここまで、ライブラリアップデートの重要性と、それを日常的に行うための具体的な方法について考察してきました。 ライブラリアップデートは、単なる保守作業ではなく、ソフトウェアの品質、セキュリティ、そして最終的にはユーザー体験を向上させるための不可欠なプロセスです。
その中で何度か触れましたが、ライブラリアップデートを頻繁に行えるようになるには、そもそもソースコードの変更容易性が必要なのです。なぜなら、ライブラリのソースコードもソフトウェアを形作る一部だからです。
裏を返せば、変更が容易なソフトウェアはライブラリアップデートを日常的に行える基盤が揃っているはずです。日々の開発とライブラリアップデートを別の仕事と捉えず、常に新しいリリースを検知し、小さな差分を取り込み続けていく。それらを日常に盛り込んでいきましょう!
*1:言語やツールによります。