フューチャー技術ブログ

ソフトウェアのバージョンと開発スタイルの関係について

はじめに

初めまして、TIGに所属している原木と申します。
バージョンという言葉を身近に聞いたり、体感することは多いのではないでしょうか?

  • 霜踏み弱体化前のバージョンでRTAの記録をたたき出したらしいね。最新版ならどんなビルドがおすすめだと思う?(執筆時は三月でした)
  • 今日新作ゲームが配信されるぞ、帰ったら遊ぶんだ…ネットワーク機能を使うためには”本体の更新が必要です。更新しますか?”

ソフトウェア開発においてもバージョンは決して避けて通ることはできません。しかし、プログラムにバージョンをつける側に立ってみると、そもそもバージョンとは何なのでしょうか?ふわっと考え出すとわりときりがないと思います。

  • バージョンの付け方ってどんなルールがあるのだろうか?
  • バージョンをつけるタイミングは?
  • バージョンってだいたいどれくらいの期間で上がるのだろう?
  • バージョンって1(or0.1)から始めないといけないのか?
  • バージョンが何回更新されるまで同じソフトウェアだと言えるのだろうか?

本記事ではソフトウェア開発において、避けて通ることのできないソフトウェアのバージョンについてそんな疑問に答えていきたいと思います。

セマンティックバージョニング

ソフトウェアのバージョンに関する命名規則は、昔から開発者の心をつかんで離さない、ホットトピックの一つです。

Software versioningというウィキペディアの記事を見ると、古今東西、ソフトウェアのバージョン管理方法として様々な手法が試されてきたことがわかります。その中でも、今日特によく目にするのがセマンティックバージョニングです。

コンテナオーケストレーションを実現するKubernetesの最新版となる「Kubernetes 1.23」正式版がリリースされました。
https://www.publickey1.jp/blog/21/kubernetes_123ipv4v6podapiv2kubelet_cri_api.html

プログラミング言語「Go」の最新版「Go 1.18」が、3月15日にリリースされた。
https://forest.watch.impress.co.jp/docs/news/1395812.html

ソフトウェアのバージョンにおいて、昨今ではこのように小数点を突き抜けてる書き方をよく目にする機会が多いと思います。
この表記方法をセマンティックバージョニングといいます。Githubの共同創業者であるTom Preston Werner氏が2010年に提唱し、GitHub上のオープンソースソフトウェア(OSS)等で広く使われるようになりました。

セマンティックバージョニングでは、 X.Y.Z (Major.Minor.Patch)というフォーマットで書きます。具体的な説明として、Go言語のモジュールにおけるバージョン番号の付け方に関する説明から引用します。

https://go.dev/doc/modules/version-numbers

リリースされたモジュールは、下図のようにセマンティックバージョンニング・モデルでバージョン番号を付けて公開されます。
セマンティックバージョニング
次の表は、バージョン番号の各パーツが、モジュールの安定性と後方互換性をどのように示すかを説明したものです。

バージョンの段階 開発者へのメッセージ
開発中 自動的な疑似バージョン番号 v0.x.x このモジュールがまだ開発中であり、不安定であることを示します。このリリースは、後方互換性や安定性を保証しません。
メジャーバージョン v1.x.x 後方互換性のない、公開APIの変更を示します。このリリースは、以前のメジャーバージョンとの後方互換性を保証しません。
マイナーバージョン vx.4.x 後方互換性のある、公開APIの変更を示します。このリリースは後方互換性と安定性を保証します。
パッチバージョン vx.x.1 モジュールの公開APIや依存関係に影響を与えない変更を示します。このリリースは後方互換性と安定性を保証します。
プレリリースバージョン vx.x.x-beta.2 アルファ版やベータ版のような、リリース前のマイルストーンであることを示します。このリリースは安定性を保証しません。

新しいバージョンのソフトウェアを公開した時は、通常パッチを出さないため(ゲームだと即日同時リリースだったりしますが….)、最後のZを省略してX.Yと書くことが多いです。先ほどの例も厳密にはGo 1.18.0、 Kubernetes 1.23.0ですが、最後のパッチバージョンを省略したため、小数点だけど小数点じゃない、そんな見え方になっていたのでした。

ソフトウェアのバージョンの付け方はセマンティックバージョニングだけではありません。例えばLinuxはかつて、バージョンの末尾が偶数か奇数かによってソフトウェアが安定版か不安定版か(ベータ版、開発版に相当)を示していました。Microsoft Windows Vistaが登場するより前の話です。

バージョニングに関する長い歴史の中で、なぜセマンティックバージョニングがこれだけ普及したのでしょうか? その理由について、リリース間隔ソフトウェアをとりまくエコシステムという二つの視点から探ってみました。

バージョンとリリースの関係

プログラミング言語には、前述したGo以外にも、RustやRuby等セマンティックバージョニングを採用、準拠したものが少なくありません。試しにRustのバージョンが1.0以降どのように刻まれてきたかプロットしてみました。

Rustリリース

等間隔にぎっしりであることがわかりますね。
比較材料としてJavaのバージョンヒストリーを並べてみるとその違いがよくわかると思います。

Javaリリース

なぜRustはこんなバージョンの刻み方をしているのでしょうか?

その理由はRustのリリースサイクル(あるソフトウェアが改修されて次のバージョンが出る前の間隔)にあります。Rust言語とそのコンパイラは6週間で新しいバージョンがリリースされます。つまり、6週間でバージョンが変わってしまうんですね。

バージョンを0.1、 0.2と刻んでいって0.9まで来たけれども、まだリリースできないから1.0にはしたくない、そんな状況を思い浮かべてみてください。セマンティックバージョニングならば、高頻度でバージョンが更新されてもバージョン間の違いや互換性を破綻なく表すことが可能です。そんなセマンティックバージョニングの恩恵を受けるのは、実は人ではなく機械側なのかもしれません。

一年間に8回以上バージョンが変わるRustのバージョンヒストリーが人間にとってわかりやすいかというと正直微妙だと思います。"これらを振り返って、「Rust 1.10とRust 1.31を比較すると、すごく変わったねえ!」などとリリースごとに言うのは難しいです。" そこで、**エディション**というさらに視野を広げたパッケージ管理のバージョン単位がRustでは採用されています。

https://doc.rust-jp.rs/book-ja/appendix-05-editions.html

エコシステムを支えるセマンティックバージョニング

昨今のソフトウェアは単一のリポジトリのソースコードからビルドできないこと、実行できないことは珍しくありません。ライブラリを管理しているパッケージマネージャーからダウンロードしたシステムモジュールパッケージ(ライブラリ)であったり、ビルドを行うためのパラメータやテスト環境が組み込まれたビルド用のパイプラインであったり、様々なサービス、別のソフトウェアに支えられてできています。ソフトウェアを取り巻く環境を生態系に例えてエコシステムと呼ぶことがあります。

このエコシステムにおいて、セマンティックバージョニングは重要な役割を果たしています。

iOSのネイティブアプリケーション作成などで利用されるプログラミング言語、Swift(金融制裁の方ではなく)のパッケージマネージャーツールであるSwift Package ManagerのREADME.mdを見ると、このバージョン指定を雑に行った結果、”依存性地獄(Dependency Hell)”に陥ってしまったユーザーシナリオが紹介されています。

OSS全盛期の昨今、アプリケーションがとあるライブラリを呼び出し、そのライブラリがまた別のライブラリを呼び出し...とマトリョーシカみたいになっていることは珍しくありません。このソフトウェアが動くために別のライブラリが必要な状況を 依存関係(Dependency) と呼びます。 ライブラリがお互いに依存関係を持つとどうなってしまうでしょうか?身動きが取れませんよね。立派な依存地獄の完成です。(このケースだけを指して循環参照なんていうこともあります)

その一つにバージョンの固定があります。意外に思われるかもしれませんが、ライブラリのバージョンをガチガチに固定することはあまり望ましくありません。

例えば、文字を解析するライブラリであるパーサーがあったとしましょう。このパーサーを利用する、ログ出力ライブラリとファイル解析ライブラリを組み込んだアプリケーションを開発します。

しかし、問題が発覚しました。ログ解析のライブラリを作ったときはパーサーv2.0.1が最新で、ファイル処理のライブラリを作ったときにはパーサーv2.0.2が最新だったので、最新のバージョンを指定したのですが、二つのライブラリを同時に使うことを想定していなかったのです。この場合、パーサーはどちらのバージョンを指定すればいいでしょうか?

ライブラリ依存ツリー1

めんどくさいし、二つのバージョンのパーサーをいれてビルドしてしまえ…アプリケーションでエラーが起きました。v2.0.1とv2.0.2はほとんどのソースコードが一緒なので、メソッド名やクラス名が重複してしまったからです。

ライブラリ依存ツリー2

これに対する解決策の一つとしてセマンティックバージョニングの活用があります。機能やIFといった大まかに変わらない範囲内でバージョンをゆるーく設定することで依存性地獄を回避することができます。

Swift Package Managerで、v2.0.0~v2.0.1までのバージョンで最新のライブラリを指定する方法を下記に示します。

import PackageDescription

let package = Package(
name: "Parser",
// いろいろな書き方があります
dependencies:
// v2.0.0以上v2.1.0未満の最新バージョンを指定する
.Package(url: "git@github.com:foo/ParserPackage1.git", versions: "2.0.0"..<"2.1.0")
// メジャーバージョン2, マイナーバージョン0に合致する最新バージョンを指定する
.Package(url: "git@github.com:foo/ParserPackage2.git", majorVersion: 2, minor: 0)
]
)

開発時とリリース時

今までセマンティックバージョニングについて説明してきましたが、ソフトウェアがリリースされた後につけるバージョンを前提に話を進めてきました。

日常的に目にするソフトウェアは、テストがちゃんと行われて動くようになったから一般的に広く使っていいよということが保障されています。これをGA版(General Availability)といいます。

GAがあるのであれば、当然GAじゃない、開発途中に対する呼び名もあります。

アプリケーションソフトウェアの開発は、さまざまな状態/段階を経て完成します。
その段階/状態と、リリース後の状態/段階を示したのが、バージョン表記です。
Pre-Alpha(Nightly Build)
Alpha
Beta
RC(Release Candidate)
RTM(Release to Manufacturing)または、GM(Golden Master)
GA(General Availability)
https://atmarkit.itmedia.co.jp/ait/articles/1003/26/news106.html

リリース時とは異なり、リリースするまでの開発時につけるバージョンはセマンティックバージョニングに完全に従うよりも、開発現場の都合を優先することが多いと個人的に思います。

筆者が以前お世話になっていたところでは、Kubernetes環境で動くコンテナイメージのバージョンについて開発中は、カレンダーの日付とgitのコミットのハッシュ値を組み合わせたものを使用していました。

コンテナイメージをpushするためのコンテナイメージレジストリを定期的に掃除して、ストレージ容量を抑える必要があったのですが、カレンダーの日付が先頭にあれば古いものを消すときに指定が楽だったからです。

もちろん、セマンティックバージョニングの枠組みでバージョンを運用することもあります。

例えば前述したRustでは、stableというバージョン以外に
nightlyビルドという毎日その日の夜に毎晩ビルドしたバージョンと
betaビルドという次のリリース機能がお試しで入ったバージョンがあります。
セマンティックバージョニングのプレリリースバージョンに続けて
ソースコードのリリース日とgitのハッシュ値が表示されるため、わかりやすいものとなっています。

soharaki@NOTE:~/work$ rustc --version
rustc 1.59.0 (9d1b2106e 2022-02-23)

soharaki@NOTE:~/work$ rustc --version
rustc 1.60.0-beta.6 (7bccde197 2022-03-22)

soharaki@NOTE:~/work$ rustc --version
rustc 1.61.0-nightly (1d9c262ee 2022-03-26)

開発プロセスとバージョンの関係

ソフトウェアをどういう過程で開発し、リリースするか?この一連の流れを開発プロセスといいます。アジャイルやウォーターフォールは開発プロセスの具体的なやり方の一つです。

この開発プロセスとバージョンには大きくかかわりがあります。

例えば、 ECMAScript(エクマスクリプト)の略称で知られる、JavaScriptの標準規格があります。このECMAScript、途中まではES1, ES2, というバージョン表記でしたが途中で名前がES2015, ES2016という風に名前が変わりました。

バージョンのタイムラインをここで見てみましょう。

ECMAScriptリリース

バージョン間隔が飛び飛びですが2015年を境にほぼ等間隔になっていることがわかります。

現在のECMAScriptはTC39という専門委員会が新しい仕様を一年かけてブラッシュアップして、その上の組織にあたるEcma Internationalが一年に一回、総会(General Assembly)にて採択しています。しかし、かつては様々な混乱があり、採択できないことが多々ありました。一年に一回、ちゃんと仕様書として出せるように策定プロセスが整ったのは2015年のことです。それ以降、ES2016、ES2017..と年がバージョン名として採用されるようになりました。

最新版のES2021/2022については、柏木さんが詳しく解説しています。
https://future-architect.github.io/articles/20210617a/

ES4は意見がまとまらず途中で放棄されたため、仕様書はドラフト版しか存在しません。


前述のJava(JDK)もある時期を境に開発プロセス、リリースの間隔を変更しています。

https://www.oracle.com/jp/technical-resources/article/java/ja-topics/jdk-release-model.html

ソフトウェアや開発体制が成熟したことで、バージョンアップが遅くなったソフトウェアもあれば、早くなったソフトウェアもあります。例えば、Rustは実は開発当初、12週間のリリースサイクルを採用していました。ですが、GA版を迎えたことで、変化を迎えます。

Rustをより早いスピードで機能開発し、同時に安定性を求めるため「リリーストレイン」という開発手法を採用しました。

…その結果、Rustは6週間!!という間隔でバージョンアップを行うようになります。
https://rust-lang.github.io/rfcs/0507-release-channels.html

「リリーストレイン」では数週間から数か月という短い時間の中で、決まった期間にソフトウェアのリリースを行っていきます。「リリーストレイン」自体が安定した定期的なリリースを実現するための手法ではありますが、なぜRustではより短い時間間隔を採用したのでしょうか?

ソフトウェアの更新頻度と人気の関係

OSSの人気を集める上で、バージョン更新の頻度自体が欠かせない要素かもしれません。

ここにGitHub上のJaraライブラリをベースに、バージョン更新がAPIの互換性、そしてライブラリを利用するユーザーにどれくらい影響を与えているかを調べた2017年の調査論文があります。

Historical and Impact Analysis of API Breaking Changes: A Large-Scale Study

この論文によると、実世界の317のJavaライブラリ、9000のリリース、26万のクライアントアプリケーションを対象とした大規模な分析により、
(i) API変更の14.78%は旧バージョンとの互換性を破壊していること
(ii) API変更の破壊頻度は時間とともに増加すること
(iv) API変更の破壊頻度が高いシステムほど大規模、人気、活発であること
などがわかったそうです。

要はバージョン更新の頻度が大きく修正を要求されるソフトウェアほど、より利用者に人気であり、開発に協力してくれる人も集めていたというものです。変化し続けるマインドを持つOSSがプロジェクトもコミュニティも成長することができると言いきっていいのかもしれません。

逆に言えば、ソフトウェアを安定的に塩漬けしたい、そういった作業に従事していただける開発者をOSSにおいて求めるのは、文化的にも人材的にも難しいという現実があります。

例えば、Go言語の父とも呼ばれるRob Pike氏は、Go言語にLTSがほしいというissueに対して次の通り回答しています。

また、ソフトウェアのビルドに必要なライブラリやその他の依存物の一式をサポートする必要があります。今なら、1.16からのコアライブラリが古いコンパイラで引き続きコンパイルされ、正しく動作することを期待するのは、大きな要求ではありますが、妥当なことかもしれません。しかし、それにはサポートを継続することに同意する貢献者文化も必要です。それを実現するのはかなり難しいようです。
https://github.com/golang/go/issues/47942#issuecomment-905184706

Goリリース

バージョン更新が当たり前の世界で守りたい約束

semver(セマンティックバージョニングのこと)は実際のところ、
メジャーバージョンアップ: 「おそらく多くの場所でコードの更新が必要になるだろう」、
マイナーバージョンアップ: 「ほとんどの部分で常に問題がないはずだ」
ということを意味します。

https://github.com/microsoft/TypeScript/issues/14116#issuecomment-292581018
※なぜTypeScriptはセマンティックバージョニングを採用しないのか?という質問に対する中の人の解答

セマンティックバージョニングを単純に採用するだけでは、ソフトウェアの安定性や互換性を担保するものにはなりません。コンベンショナルコミットといったソースコードの修正をわかりやすくするコメントの書き方であったり、ビルドパイプラインでテストをなるべく自動化することで意図しない破壊的な修正=デグレを防ぐような仕組みが別途必要です。

それらの仕組みを設けてセマンティックバージョニングに準じようとしていても開発の過程でマイナーバージョン更新によって今まで使えていたAPIが使えなくなるといったことはあります1…ですが、そういった修正を繰り返すと利用者側(開発者も含む)の信用は当然すり減っていきます。

「メジャーバージョンが変わらないから、影響はそんなにないと思っていたのに…」

バージョンをパッと見たときのユーザーの暗黙的な期待値を下げないための工夫の一つとして、メジャーバージョンが同じ間は、最低限この機能は影響が及ばないように品質を保証しますといったことをドキュメント化する手立てがあります。

例えばクラウドの構築などで利用されるTerraformでは”Terraformv1.0の互換性の約束”として、メジャーバージョンが1の間、互換性を維持する内容について明文化しています。
https://www.terraform.io/language/v1-compatibility-promises

まとめ

ソフトウェアのバージョンの付け方について、主にセマンティックバージョニングを中心に開発手法を交えて説明しました。

  • バージョンの付け方ってルールがあるのだろうか?
  • バージョンをつけるタイミングは?
    • 開発時~リリースまで目的に応じた様々な段階があります。
  • バージョンってだいたいどれくらいの期間で上がるのだろう?
    • 開発を行うプロジェクトがどういった開発手法を採用しているかによります
    • 人気のあるOSSは「リリーストレイン」と呼ばれるリリース期間を固定した方法を取っているところが多いようです。だいたい1ヵ月~6か月でマイナーバージョンが上がります。
  • バージョンって1(or0.1)から始めないといけないのか?
    • セマンティックバージョニングに従えば、0.1.0から開発版をリリースして、マイナーバージョンを上げていけばいいと一般的に言われています
    • セマンティックバージョニングに従うのでなければ、開発者の気持ち次第です。

ソフトウェアやサービスの開発現場に配属後、バージョンを意識し、時には互換性といった問題で悩む機会は多々あると思います。本記事はそういったトラブルを具体的に解決するものではありませんが、そういったルールで回ってるんだと頭の片隅にあれば、問題を意識しやすいのではないでしょうか。

少しでもお力になれば幸いです。

閑話休題: 最初のバージョンは1から?

バージョンが1.0になるとテストがちゃんと行われて動くようになったから一般的に広く使っていいよということが慣例で決まっています。これをGA版(General Availability)といいます。
ですが、SuSE Linux(Jurixベース版)の最初のリリースのバージョンは4.2でした。これはSF小説「銀河ヒッチハイク・ガイド」をフィーチャーしたものです。
https://en.opensuse.org/S.u.S.E._Linux_4.2

閑話休題: 一見セマンティックバージョニングっぽいけど…

TypeScriptは型の構文を備えたJavaScriptであり、Webアプリケーションの開発などで活躍しているプログラミング言語です。
TypeScriptのバージョンは一見するとセマンティックバージョニングに見えます

soharaki@NOTE:~/work/example-20220328$ tsc --version
Version 4.6.3

ですが、実態は十進数的なdecimal versioningです。

https://twitter.com/teppeis/status/1296672623498149888

TypeScriptリリース

閑話休題: バージョンといえば、リリースノート芸も忘れてはなりません

ソフトウェアのバージョンを上げたとき(リリースした時)、どんな機能を更新したのか?
説明する文書をリリースノートといいます。

企業によっては知られざる文才が密かにその実力を発揮しているようです…
例えば、チャットアプリケーションのSlackの履歴を見てみましょう。

Slack 22.03.10 2022年3月14日 新着情報
チャンネル参加前にプレビューする際、チャンネル名とともに説明が表示されるようになり、より詳しい情報を得られるようになりました。これなら「#たぬき」が信楽焼のチャンネルなのか、カップ麺の話なのか、それとも策略家の集いなのか、一目瞭然ですね!

「明日」や「来週」などにリマインダーを設定した場合、その表示時間を選べるようになりました。始業が朝 9 時でない皆さんや、Slackbot のアラートが朝一に飛び込んでくるのがしっくりこない人にぴったりです。「環境設定」>「通知」で、好きな時間をデフォルト設定してください。
https://slack.com/intl/ja-jp/release-notes/ios

新しくリリースされた機能がユーザーのどういった利用シーンで活躍するのか非常にわかりやすいですね。

このリリースノートから、Slackは一か月単位で機能をリリースしているリリーストレインスタイルであること、バージョニング方法はカレンダー方式(これはOracle等で採用されている昔からある方式)であることがわかります。

参考文献


  1. 1.https://ieeexplore.ieee.org/document/6975655 JavaのパッケージマネージャーであるMavenで、後方互換性を意識したセマンティックバージョニングをちゃんと行っているライブラリがどれくらいあるのか調べた調査論文