Kengo's blog

Technical articles about original projects, JVM, Static Analysis and JavaScript.

Javaプロジェクトにおけるリリース周りの手法あれこれ

考慮する点

成果物のデプロイ

ビルドの成果物(artifct)をアップロードすること。アップロードと公開は分けて考えることに注意。デプロイ先にはいくつか候補がある:

  1. GitHub Packages (旧GitHub Package Registry)
  2. Maven Central Repository
  3. Docker HubなどのDocker Registry

GitHub Packagesはコンテナも.jarもまとめて置けるが、コミュニティ標準の場所ではないので利用する際にひと手間必要になる。プライベートプロジェクトの場合は積極利用することになりそう。FOSSなら基本的にMaven Centralに置くことになる*1。プロジェクトによっては.jarファイルとしてではなくコンテナとしてデプロイすることもあるだろう。

リリースノートの作成

CHANGELOG.mdsrc/site以下のファイルを手動でメンテナンスする手法だけではなく、コミットコメントやPRからリリースノートを作成する手法もある。

  1. 手動管理
  2. CHANGELOG.md などファイルの自動生成
  3. GitHub Releasesによるリリースノート自動生成

特に開発者向けであれば、CHANGELOG.mdではなくGitHub Releasesを使うことで、変更内容の伝達を自動化できる。例えばDependabotは依存先の更新を提案する際にリリースノートの内容をPRに含めてくれる。ファイルの頒布が必要な場合でも、GitHub Releasesも合わせて使ったほうが良い。

バージョンの管理

利用者の利便性のため、Semantic Versioning 2.0を使うことが前提になる。ただ混乱を避ける意味でもバージョン 0.x.x の利用は避けたほうが良いかもしれない

  1. 手動管理
  2. 自動的に決定する

バージョンを決める作業自体は自動化が進んでいて、Conventional Commitsから生成するものがsemantic-releaseだけでなく様々あるので使ったほうが良い。私はプラグイン機構がしっかりしているのでsemantic-releaseを使っている。

プロジェクトのバージョンをpom.xmlbuild.gradle, gradle.propertiesといったファイルで管理するのが普通だが、最近はGitのTagを見るものもある。Tagを見ることで「ファイルに書いてあるバージョンをインクリメントする」ためのコミットが不要になるのが利点。例えばDraft Releaseを本Releaseに昇格させる(ブランチの最新コミットにタグを打つ)ことで公開処理を回す(tagイベントに紐付いたワークフローを回す)処理がシンプルになる。のだが、SNAPSHOT運用と相性が悪いのと、ファイルに書かれたversionの信頼性がなくなるのとで、あまりJavaプロジェクト向きではないと考えている。

各手法について

Maven (maven-release-plugin)

mvn release:prepare && mvn release:performを実行するだけで必要な処理が完結する。Maven公式の機能で完結し、歴史も長いのでノウハウも蓄積されている。

シンプルだが、Tag作成をリリースの起点にできない(Tag打ちがプラグインによって行われる)ために自動化には向かない。多くの場合、リリース用の(Jenkins)Jobを作って手動で蹴るような運用になるのでは?pom.xmlの更新とGitへのpushがプラグインによって行われることもあり、自動化の際はプラグインによるコミットメッセージに[skip ci]を含めたり設定を変更したりといった工夫が必要になる。

Maven (maven-deploy-plugin)

Mavenでリリースを自動化する場合はmaven-deploy-pluginによるデプロイ手法を採ることになる。特にmaven-versions-pluginのsetゴールと組み合わせることが多いはず。

リリースに使用するバージョンをTag名などを経由してnewVersionプロパティに渡してversions:setを実行、deploy:deployでデプロイした後に再度versions:setでSNAPSHOTバージョンに変えてGitにpushする運用。release-drafterが使えるのでリリースノート管理もしやすい。

この場合でも、Tagが打たれたrevisionのpom.xmlには実際にリリースされたversionとは異なる値が書いてあるはず。versionの信頼性という点で問題が残る。

Maven (maven-semantic-release)

version mismatchが気になる場合は、semantic-releaseの利用を検討する。これはmasterブランチにpushされた変更をすべてリリースするという手法である。

バージョンの決定はsemantic-release本体が、リリースノートの作成は@semantic-release/github@semantic-release/release-notes-generator@semantic-release/changelogが、GitのcommitやTag打ちやpushは@semantic-release/gitが行うことになる。すべてのプロセスが自動化されるので、運用の負担は最も低くなる。またversionの信頼性も確保できる。

Conventional Commits(厳密にはAngularのルール)の利用を全開発者に強制しなければならないこと、Javaプロジェクトなのにpackage.jsonを使わなければならないこと、masterブランチを常にリリース可能にすることが許容できるならばこの手法が最善と思われる。

ライブラリはともかくアプリやサービスだとPR毎リリースが受け入れられないことはあると思うが、こちらのFAQに目を通した上でも必要と判断されるなら、リリースを意図的なタイミングでトリガする手法を取り入れることもできる。

Gradle (gradle-semantic-release-plugin)

semantic-releaseはGradleでも使える。詳細は以前の投稿を参照。

Jib

GitHub repoにBuild container images for your Java applicationsと書いてあるように、Javaアプリをコンテナ化するときに使えるツール。Maven/Gradle両対応。デフォルトでもコンテナのレイヤをうまく分けたり、warプロジェクトならJettyを同梱したりしてくれる。

依存を必要としないのはありがたいが、dockerひとつ追加するだけならあまり苦ではない(GitHub Actionsだとデフォルトで付いてくる)のでそこまでの利点という感じではない。ドキュメントにちょいちょいKubernetesとの組み合わせ方について言及されているので、Kubernetes使いにとってはやりやすいところがあるのかも。

Docker

MavenやGradleを使ってデプロイするのではなく、CIサービスでdockerコマンドを実行する。Maven/Gradleはpackage/assembleするまでが責務で、デプロイはdocker pushで行う運用になる。前述の通りdockerコマンドの導入は多くのCIサービスでfirst classのサポートを受けているので、セットアップは非常に楽。最近はmulti-stage buildもあるので最終成果物も充分に小さくしやすい。

一見やりやすいが、開発作業中にdockerコマンドが実行されない可能性をはらんでいる。これは開発者が触るものがリリースされるものと異なるものになるということで、微妙なリスクになる。開発作業中もmvnw/gradlewではなくdockerコマンドを使うようにする(付随する課題も解決する)か、コンテナにデプロイされたアプリを使うE2Eテストを早期に統合すると良いかもしれない。

なおイメージを小さくするのに jlink を試してみたいのだが、現状ではalpineで動くOpenJDK(Portolaプロジェクト)が公開されてない?ようで、2018年5月時点で有効だった手法が使えなくなっている。のでalpineイメージをベースにするためにはDockerfileがけっこう複雑かつ管理の難しい状態になってしまうのが難点。この辺の情報はおそらくこの2018年11月時点のブログ記事が最新。

docker-compose

コンテナの利用をもう一歩進めて、データストアやHTTPサーバもコンテナ化してアプリとの依存関係やそのバージョンをdocker-compose.ymlで管理する。 ここまで行くとより良いデプロイ手段というよりは、より良いプロビジョニングやポータブルな実行環境の方を実現したい段階だと思われる。

注意することはDockerを単体で利用する場合と同様だが、アプリのコンテナが依存するコンテナ(データストアなど)をアプリをビルドするたびにビルドするような運用にならないように注意する必要がある。具体的には、ADDするファイルを最小限にすることで、スキーマ定義やマイグレーションスクリプトが変更されたときなど、必要なときだけ依存するコンテナがビルドされるようにする。

*1:Maven Centralも十分に早いしHTTPSもサポートされたので、個人的にはJCenter使うモチベーションがない