「ソフトウェアの設計の陳腐化」という言葉はよく言われます。ドキュメントがなく引き継ぎがなくという周りの人間の問題を除外すれば、ライブラリや処理系のバージョンアップ問題もその要因の一つだと思います。それ以外には機能拡張によるつぎはぎの問題、世間の変化に遅れる(世間ではウェブになっているのにデスクトップアプリであったり)とかいろいろあるとは思いますが、とりあえずここではバージョンの話だけ取り上げます。
ここに書いた話は、Qiitaにくるような人はだいたい知っている話とかが多いと思います。非技術者なステークホルダーにも、ライブラリや処理系のバージョンアップ、ソフトウェアの陳腐化とは何かみたいな話をしなければならない場面はそれなりにあると思いますが、あまり理由をきちんと説明している文章をみたことがないです。「陳腐化するんですよ!それがソフトウェア業界の常識です」みたいなトートロジーで煙に巻く人も多い気はしていて、それはそれでよくないと思うんで、そのときに「これ」と雑にリンクを送れば説明できる文章になったらいいな、と。
この文章では特にリリースサイクルが短く、Long Term Supportとか言いつつ1年前後だったりするフロントエンドの環境を主に想定しています。ちなみにソースコードが出てこないのでポエムです。
なぜライブラリのバージョンの管理が必要なのか(バージョン固定じゃダメなのか)
プログラムを開発するときは、他のツールやライブラリを当たり前に使います。これは今に始まったことではなく、はるか昔からそうですね。OS組み込みの機能だけで開発するとしても、OSベンダーの提供する開発ツール、OSの機能を使うライブラリ(API)は最低限使います。
例えば、Javaで開発する場合、Javaの言語、言語組み込みのライブラリは使いますし、Gradleみたいな別なビルドツールやらも使います。SpringBootみたいなライブラリも使います。それぞれ、どのバージョンを使うかというのをスタート時に決めますし、メンテ期間等で見直しをする必要がでてきます。
なぜ固定ではダメかというと、主に2つの理由があると思います。
機能的な問題
それぞれのツールやライブラリは、それぞれの開発元が考えるライフサイクルで更新されていきます。そのタイミングで、機能が追加されることもあれば、過去のバージョンで提供されていた機能が削られたり、挙動が変わったり、というのがありえます。その過去のバージョンがもう手に入らない、ということもありえます。
ウェブの場合だと、アプリケーション側でコントロールできないものにブラウザバージョンがあります。ある程度は使用バージョンを固定するなども業務システムではありますが、古いブラウザでしか動かないとかはダサいですよね。
例えばFlashを使っていると、もう動かすことはできません。実装していた機能を取り除いて、その互換実装に置き換える、という作業が発生します。
もっと小さい例でいえば非推奨になっているReactの特定のライフサイクルメソッドの関数(componentWillMount
)を使っていたら、React 17が出るとそのアプリケーションは動かなくなってしまいます。これはReactのバージョンを固定してしまえばなんとかなるのかもしれませんが、追加の機能を入れようとして別のライブラリを入れようとしたときに、それがReact 16では動かなくて、React 17しかサポートしていないと、そのライブラリが使えないということになります。4Kブルーレイを見たいけど、うちの古いブラウン管テレビにはHDMI端子がなくてプレイヤー繋げられないわー、みたいな感じのことが起きます。
時間が経てば経つほどそのようなものが多くなってきます。まとめると、以下の2点ですね。
- 既存機能が動かなくなる
- 世間一般では普通の新規機能の追加が困難になっていく
セキュリティ的な問題
インターネットがなかった時代・接続しない時代は良かったんですが、今ではネットワーク前提のシステムが大幅に増えています。それにより、今までよりもセキュリティのリスクにさらされる機会は増えています。また、ネットワークに直接アクセスしないシステムであっても、USBメモリ経由でやってきたワームの攻撃を受けるなどがあります。
セキュリティに関しては存在(&攻撃方法)が報告されているセキュリティホールを放置して、システムを危険にさらされると、システムの提供元や開発元が責任を追求されることになります。「無能で説明できることに悪意を見出してはいけない」という格言があります。ただ、これらは「悪意」を持っていると誤解する人が多いからこそこういう言葉が生まれたのだと思います。あと、僕個人としては「時間不足で説明できることに無能を見出してはいけない」という持論があります。組み合わせると、忙しくて直せなかったとしても、「悪意があってユーザーを危険にさらしたのだ」と批判される恐れがあるということです。加害者になってしまうのです。
現代のシステムは数多くの部品で組み上げられています。ゼロからすべてのコードを自分で書くことはありません(ほとんど)。脆弱性に対する防御は社会的な仕組みが構築されています。特定のライブラリやツールに脆弱性があると、その攻撃手法などを報告する窓口があります。また、そこから開発元にこっそり連絡がいき(対策されていない時点での存在発表はそれ自体が加害行為になる)、脆弱性が修正されたバージョンのリリースと同時に公表、という流れです。
同時といっても、大きな問題は発表されたら即座に対策を取らないと、加害者になりかねません。そのためには、最新の修正済みのバージョンを入れる必要が出てきます。
問題はすごく古いバージョンのサポートまでは行われない点です。だいたい、大きめのOSSや商用のミドルウェアやライブラリを出しているベンダーであれば、きちんとサポートポリシーを定義して、バージョンごとのサポート期限を定めています。ただし、そこから外れてしまうと、よっぽど大きな問題でない限りは更新が提供されないことがあります。
まとめると以下の点になります
- セキュリティの修正が提供されずに、システムに穴が開いたままになりかねない
バージョン
現在提供されているシステムの多くは3つの数字を並べたバージョンを使っています。
- x.y.z
xをメジャーバージョン、yをマイナーバージョン、zをパッチバージョンと呼んだりします。例えば、12.0.4とか、3.7.3とかそういうやつです。
Windowsは商品名としては95とか2000とか10とかつけたりもしますが、内部的には2つの数字の列になっています。18362.175とかそういうやつです。
数字付けのルールは各システムが勝手につけることが多いので、全部のシステムで統一的なルールというのは、大きい数字ほど新しい、ぐらいのものです。昔はxが偶数が安定板、yが奇数が開発版みたいなのがよく使われたりもしていましたが、マーケティングの都合でいきなりxが大幅にジャンプしたりとかあります。xが上がると後方互換性がないバージョンアップだが、yの更新は後方互換性があるとかもよく見かけます(セマンティックバージョニング)が、気分でxをあげるシステムもあります(Linuxとか)。
フロントエンド開発でよく出てくるルールがセマンティックバージョニングです。
バージョンのサポートの考え方
サポートの考え方は大きく3種類ぐらいですね。
- 最新メジャーバージョンのみサポート
- 最新のいくつかのメジャーバージョンのみサポート
- 最新のメジャーバージョンと、特定の不連続なメジャーバージョン(LTS)のみサポート
たいてい、メジャーバージョンごとにサポート期間を設定することがほとんどです。開発リソースの多いプロジェクトでは、複数メジャーバージョンを同時サポートします。小さいプロジェクトや個人プロジェクトでは最新バージョンのみサポートというケースがほとんどです。また、変化の早いブラウザも最新バージョンのみです。
最新のいくつかのメジャーバージョンというのは、例えばOracle社製のデータベースは最新2バージョンのみサポートとかそういうやつです。ただ、ウェブのフロントエンド開発ではあまりみないかもしれません。
よく見るのがLTS(ロングタームサポート)という長期サポートバージョンを定めているライブラリとかツールです。
Node.jsは、現在の半年ごとにメジャーバージョンアップします。最新のメジャーバージョンのものはcurrent扱いです。奇数バージョンはcurrentでなくなってすぐにサポートが終わりますが、1年に一回出る偶数バージョンは、currentでなくなると(次のバージョンが出ると)LTSになり、2年半サポートされます。現在の最新は12ですが、これはまだLTSではなくて、13が出ると12がLTSになる、というのは要注意です。12はメジャーバージョンアップですが、現在も活発に機能追加が行われていますので、LTSにはなっていません。
ライブラリではAngularがすでにLTSを含む運用をしており、現在最新の8は、次の9が出るとLTSになって、その後1年サポートされます。Vue.jsも、3.xが出たら2.xの最終盤がLTSとして18ヶ月サポートされると宣言されています。
バージョン選びの作戦
Node.jsやアプリケーションで使うパッケージのバージョン選びの戦略は主に3つあります。バージョンアップ作業には時間がかかります。バージョン更新そのものではなく確認の工数もかかります。その分、新機能開発の工数は削減されます。時間がかかるということはそれに対して費用も発生します。どこの費用を使ってやるか、どこに請求するか、稟議をどう投げるかの考慮が必要です。なので、それをどこで消化するかを決めるのがバージョン選びの大切なところです。「そんなの決めなくてもなんとかなるよ」というのは、チーム内の誰かの善意(やる気)に甘えているだけなので要注意です。
いくつか考えられる作戦を列挙してみます。どれか一つを選ぶというよりかは、状況に応じて複数のパターンを利用する感じで考えています。
最新バージョンを積極的に選ぶ
常に最新バージョンを取り込んでいくスタイルです。バージョンアップタスクの分散化です。日々最新のものを取り込むスタイルです。例外としてはiOSのモバイル開発で、最新iOSのGMが出てから、ユーザーの手に最新のOSが渡り始める1-2週間で動くものを作って提出しなければなりません。あと、Reactとか、安定版などなく、最新版のみが更新されていくライブラリがコアとなると必然的にこうなります。
新しいバージョンを試してその知見を公開するだけでもありがたがられるというメリットもあります。バグ報告とかでそのソフトウェアに貢献もできるかもしれません。類似パターンとしては、ベータ版も試すパターン、最新のmasterのバージョンも試すパターンもあります。
ただ、現在は複数のライブラリを組み合わせて行うため、新しすぎるバージョンでは他の連動して動くライブラリの準備ができていないケースもあり、予想外に時間が取られることがありえます。特に、Babelなどの影響の大きなライブラリのバージョンには要注意です。以前、Next.jsが、Babel 7 beta42だかの中途半端なバージョンに依存して、いろいろ食い合わせが悪くて苦労した思い出。
LTSを中心に組み立てる
メインの部分(Node.jsとかAngularとか)をLTSで固め、その周りをそれに準拠する形で固めていきます。メリットとしては、スケジュールが見えているので、あらかじめバージョンアップのタイミングを計画に折り込みやすい(年間予算の計画が立てやすい、稟議にかけやすい)というものがあります。LTSのリリースの前には安定化のための期間が置かれており、比較的問題が起きにくいでしょう。
ただ、LTSが提供されているものならこれでいけますが、現状、LTSを提供しているコアのライブラリはあんまりないので、現状はAngularを使っている場合のみしか適用できません。Vueはそのうち始まりますね。Reactの場合は、LTSはないが、きちんと検証された組み合わせであることを期待して、Next.jsのメジャーバージョンアップに淡々とついていく、という方法はあります。
LTSを使う場合も、LTSの範囲内で最新のものを積極的に使うか、固定化するかみたいな細かい作戦の差はあります。
(参考)クリーンルーム
まず最初に書いておくのは、ここでいうクリーンルームというのは、リバースエンジニアリングでコピー実装を作る手法でもなく、検証可能な高品質なソフトウェアを作るという高尚な手法でもなく、SIer用語です。
インターネットやネットワークない部屋に集めて、外部のライブラリなどを使わずに、最低限のツールや言語を元にしてシステムを作り上げるというたまに話題になる例のあれです。昔は銀行系ではそういうのがあったとか、あのお客さんはそうだったとか、今でもあるようだとか噂はいろいろ聞きますが、うちの会社ではないです。
きちんとしたセキュリティの知識がない人が作ると、たいてい、世の中で報告されるセキュリティホールの原因となるようなバグを入れ込んでしまいます。世の中のライブラリやツールもセキュリティホールを作ろうとして穴を作ったわけではないですし。もちろん、すごく頭のいい人たちが集まっていて、形式手法とかマスターしまくっているような人材が集まっていて、コストとリターンがきちんと見えている現場なら良いと思います(実際、GoやRustを開発したのはそういう意図もあってのことだと思いますし)が、そういう人材が溢れている現場なんてほとんどないでしょう。あったら教えて欲しいぐらいです。
セキュリティに関しては「多くの人の目に触れて正しい実装になっているという確証が得られているコードを利用する」というのがベストプラクティスですよね。そもそも、世の中の「当たり前」のレベルがどんどん上がっているので、当たり前を作り込むコストは右肩上がりですし、コスト的にも大幅な投資超過になってしまうでしょう。
外部のライブラリがなくて自分たちで全部コントロールできれば陳腐化とかはないんだ、というのはソフトウェアの最近のエコシステムとかエンジニアリングに詳しくない人が一度はかかる麻疹みたいなもので、可能であればワクチンをきちんと接種して撲滅しないといけないやつです。
セキュリティ診断
ライブラリのセキュリティの診断は、数年ぐらい前はsnykにユーザー登録をしてsnykコマンドを実行して検知していました。現在はnpmコマンドを使ってnpm installするだけでも脆弱性のあるパッケージが検知できます。また、GitHubにソースコードをアップロードすると、GitHubが検知してくれます。
脆弱性のあるパッケージは自分が直接インストールしたもので発生するだけではなく、そのパッケージが利用している別のパッケージのさらに依存しているパッケージが・・・みたいな依存の深いところで起きがちです。特にウェブフロントエンド開発をしていると、依存パッケージ数が簡単に4桁とかいってしまうので、すべてを目視で確認するのは難しいです。自動検知を活用しましょう。
なお、検知されたすべてを修正しないといけないかというと、そんなことはありません。例えばnode-sassはrequestという外部ネットアクセスのライブラリに依存しており、これが更新されなくて脆弱性が検知されたことがありました。node-sassはCSSを書きやすくしてくれるユーティリティで、実行時には動作しません。ビルド前のCSSの中で外部リソースに依存していないのであればこのライブラリは使われないはずで、「これは検知されたが影響はありません」というように、説明がつけばOKです。
セキュリティ目的の自動バージョンアップ
npm installの脆弱性診断で何かしらを検知したら次のコマンドで可能なものを修正します。
$ npm audit fix
これだけでちょっとした修正は完了できるはずです。メンテナンスがあまりされていないパッケージの場合は、脆弱性ありで修正がないまま放置されていたりします。あるいは、脆弱性ありのバージョンに依存したまま、ということもあります。こうなると npm audit fix
をしても脆弱性があるモジュールでも修正できなくなってきます。それをトリガーにして一部のパッケージのメジャーバージョンアップが必要となります。もちろん、他の戦略をとっていても重大なセキュリティの場合には応急処置せざるを得ない可能性があります。まあ、セキュリティの緊急度が高いほど、いろんなメジャーバージョンに対してパッチが発行されることもあり、逆に簡単かもしれません。
あまりにも脆弱性が放置されているライブラリがある場合は、バージョンアップではなくて、類似の別のライブラリに置き換える、というのも選択に入ります。
バージョンアップ時のトラブルを減らす
バージョンアップでのトラブルを完全にゼロにはできません。ただ、日頃からの心がけで少し楽にすることはできます。
CIをしておく
普段からCIをしておくことで、いざバージョンアップ時の確認の補助に使えますし、最近は、利用されているモジュールの中に脆弱性のある古いバージョンが紛れていないかの自動検知が行えるようになってきています。
JavaScriptと比べたTypeScriptの場合、一番有利なのがここですね。ライブラリのAPIが変わってビルドができない、というのが検知できるのがメリットです。もちろん、ロジックなどが正しく動くかどうかというテストもあるに越したことはありません。
こまめにバージョンアップ
セキュリティのバージョンアップ、パッチバージョンアップなど、小さい修正はこまめにやっておけば、いざというときにあげるバージョンの差が小さくなります。例えば、1.6.5から1.6.6で、0.0.1だけあげたら問題が起きた、と分かれば、エラーの原因の追求、問題の報告が極めて簡単になります。
人気のある安定しているライブラリを利用する
身もふたもないのですが、APIのbreaking changeが頻繁に行われないライブラリを選べば楽になります。後方互換性やサポートポリシーについて言及があるライブラリが良いです。あと、人気があるライブラリであれば、バージョンアップで困ったときに情報が入手しやすくなります。
ライブラリやフレームワークを浅く使う
ライブラリやフレームワークを使う場合、メジャーな一般的な使い方からなるべく外さないようにします。間違ってもライブラリをラップして完全なオレオレフレームワークを作るとかすると、バージョンアップ時の作業が多くなります。また、メジャーな使い方に近づけておけば、ネットで情報を調べるときにも問題が発見しやすくなりますし、チームメンバーが途中から入ったとしても、実は最初から使い方を知っている、ということも期待できるかもしれません。
(参考)式年遷宮
近年のウェブフロントエンドは、たくさんの小さなツールやライブラリを組み合わせて使うことが多いです。ライブラリの数が多ければ多いほど組み合わせの数は爆発していきます。世界であまり多くの人が試していないバージョンの組み合わせでやらざるを得なくて・・・ということも起きるかもしれません。
複雑化するにつれて、プロジェクトの新規作成を手助けするツールが提供されることが増えてきました。特にVue.jsのCLIはきちんと作り込まれています。いっそのこと、バージョンアップ作業をするのではなく、CLIツールを最新化して、それでプロジェクトを新規に作り、それに既存のコードを持ってくるという方法もありかもしれません。
なお、この方法はまだ思いつきレベルで、まだ実際に試したことはないです。
まとめ
技術的な内容はあまりなく、ふわっとした話でした。半分ぐらいには縮めると思いますが、仕事ですぐに使えるTypeScriptに追加しようと思っているネタの脳内ダンプポエムでした。