Javaのデプロイ問題
いきなりですが、Javaのデプロイってみなさんどうされてるんでしょうか。
もちろん、デプロイなんて、最終的には方法はいくらでもあります。ありますが、スクリプト言語ではgit cloneしてgemやらpipやらnpmやらで多少の依存性解決で済ませるようなライトなプロジェクトが、Javaになるとfat jarのfatさに驚きながら丸ごと落としてくるとか、jarファイルを生成して、手作業で差し替えるとか、ちょっと微妙なスタイルになってしまいます。
(そんなことないよ!とか、いい解決策があったら教えてください)
そもそもそんなときまでJavaで書くなよと言われそうですが、周辺ツールでコード資産を再利用したり、ちょっとマルチスレッドで並列計算したかったりと、軽くJava(やJVM言語)が必要なシーンというのは依然として存在します。
Javaにおけるデプロイ手法
まずは既存のJavaデプロイの手法を検討してみましょう。
ソースコード転送
スクリプト言語と同じ発想でどうにかしようという方法です。gitまたは何らかのツールでソースコードをコピー、実行サーバでgradle(以下、maven/sbt/etc.を含む)で依存関係解決して現地ビルド。場合によってはそのままgradleで実行してしまうというわけです。
利点
- 開発環境と同じ環境を作り上げられる
- gradleスクリプトで関連処理も記述できる
- 現地改修もできる
欠点
- ビルドしたバイナリが、テストしたバイナリと同一である保証がない
- ビルドしたソースがテストしたソースと同一である保証も厳密には難しい
- 時がたつと、ビルドしたバイナリがそのソースからのものであるかどうかも怪しくなる
- なぜいちいちビルドしなければならないのか。本質的に非効率
悪くはない方式ですが、一式丸ごと入ってしまうので、デプロイ先というよりはほぼ開発サーバという感覚になります。あと、サーバ内で改変されていないと明言しづらいのが困りものです。変更してビルドして、そのあと誰かが何かしようとして別のブランチに切り替えててもわからない不安さがあります。
classファイル転送
.javaファイルをコンパイルした.classファイルをサーバに転送して実行する手法です。
利点
- 手元の実行と同じように動くことが期待できる
- javacコマンドやIDEのみで生成できるので特別なビルド作業がいらない
- 一部の機能だけ更新が楽
欠点
- 転送すべきファイルが多い
- リビジョンが揃っているかどうか怪しい
- 一部だけ更新されてソースコードと同期が取れてないと地獄
- 依存関係にあるjarファイルをかき集めるのがたいへん
手法というほどのものかどうか…Javaを使い始めた頃はよくやってました。IDEのビルドしたファイルをそのままコピーするだけなので、楽なんですよね。デプロイというより、単にサーバで動かすというだけの方法。スクリプト言語なら.zipや.tarで固めてコピーするみたいなものですね。.classが全部揃ってなかったり、もちろんバージョンがよくわからなかったり、混ざったりと実運用を考えると最悪の手法です。
jarファイル生成
IDEやjarコマンドで.classファイルをまとめたり、gradleで生成させたりした.jarファイルをサーバに転送します。依存ライブラリの.jarファイルも一緒に転送します。
利点
- .classに比べて転送が楽
- ソースコードやドキュメントも格納できる
- 依存ライブラリがファイルからわかりやすい
欠点
- 依存関係にあるjarファイルをかき集めるのがたいへん
原始的な割には正当な方法です。実装的には、.classファイルをzipで固めて送ってるのと変わらないんですが、各種Javaツールが対応してくれているので扱いが圧倒的に楽です。
ソースコードやらメモやらドキュメントやら、なんでも含められるので、管理できないときはいまだに有効だと思います。ただ、依存関係にあるすべての.jarファイルを集めるのがつらいんですよね。そしてバージョン違いの.jarが混ざってDLL地獄ならぬJar地獄になったりして…
fat jarにまとめる
王道的手法。依存ライブラリもすべて一つの実行可能jarとして再構築します。mainを定義するので、次のように実行できます。
> java -jar xxxx.jar arg1 arg2
最近(?)はuber jarやshadow jarという言い方もあるみたいですね。
利点
- 単一ファイルで完結していて安全で配布しやすい
- mainを定義できる
欠点
- だいたい実際にfatになる。数十MBサイズ
- mainを一つしか持てない
- main定義以外のmainを起動するときは起動方法を変えることになる
これはよく見かける手法です。単体ファイルを転送すればそれで済むというのがいいところです。全部入りなので、部分的に更新されてひどいことになるという心配もありませんし、mainクラスを忘れてしまう心配もありません。
個人的にはファットすぎる(クソでかい)のと、実動環境だけを見たときには、使用ライブラリや実行しているmainクラスの名前がすぐにわからず、解凍してみないとなんとも言えないので、サーバで動かすときにはあんまり好みに合いません。たぶん、ちゃんとした命名規則なりのルールの下で運用するといいと思うんですが。
gradleとかで、依存関係の.jarファイルをかき集めるのが面倒くさいという理由でfat jarを選ばれることもあって、ちょっと悲しいですね。それほど一般的ではないようですが、依存関係ライブラリはこういう手法で集められます。
warファイルにまとめる
Web以外ではあまり見かけないのですが、warファイルやearファイルでもfat jarと同じことができます。warの方がアプリケーション設定ファイルを含めつつクラスパスからは除外するなど、細かい制御ができるので、向いているかもしれません。
利点
- fat jarっぽいことも、zipっぽいこともできる
欠点
- 設定ファイルを現地で書き換えた後でバイナリだけアップデートしにくい
Java EE環境が多いならwarファイルは便利だと思います。warファイルなら多少はファイルサイズが大きくても許されるのではないでしょうか(偏見)
Dockerイメージ
Dockerコンテナ内に実行環境を生成してイメージを作成します。JavaのDockerイメージなんて重い?いいえ、Alphine Linux/musc libc/Jigsawでまあまあコンパクトになります。
最近ではGoogle/JibでMaven/Gradleで一気通貫でDockerイメージを作ることもできます。
利点
- Dockerなのでよい
欠点
- Dockerなのでつらい
これからの本命でしょう。JavaのDocker対応も進んでおり、分かりやすい問題はほぼ無くなったと言えるのではないでしょうか。バージョン管理もやってくれますし、Javaローカルな動作に縛られる心配もありません。
ただし、一方でDockerなんですよね…Docker向きでないシチュエーションというのも結構ありますし、比較的素直にメモリコントロールできるJVMをコンテナに入れるのも、なんか残念感がするこの頃です。
新しいデプロイ方法を考えてみる
いろいろ並べてみると、理想のデプロイのためには次のような課題を解決したいわけです。
- バージョン管理
- デプロイしたバージョンがわからない
- jarファイルがあってもバージョンを銘打つのはプログラマ任せなので、最悪の場合、どっちのjarファイルが新しいかすらわからない
- 解析の容易性
- 長い年月が経っても、実働環境を見たら何をどうセットアップしたかわかるようにしたい
- .jarを逆コンパイルしてソースコードを生成するのはもう嫌なんです…
- 気軽さ
- そこらのサーバで適当な感覚で使いたい
- 軽くて簡単なのがいい
この要件を満たすのはどういうツールか?というところで、他の言語に目を向けてみると、普通はgemやらpipやらnpmやらでデプロイしちゃえるわけですよ。メインコードはgit cloneしてもいいですし、プライベートなリポジトリからデプロイしてもいい。バージョンはリポジトリで管理されてますし、実働環境見たら何がどうなったか一発でわかる。
Javaにはそんなのないの?と考えると、少なくともリポジトリの方はMavenリポジトリがあります。コンパイルが必要なgit cloneはできないですが、プライベートなリポジトリのサポートは他の言語より有力なような気もしてきます。あとはツールの方があればいいんですよね。。。
というわけで作ってみました。
marun
Mavenリポジトリから対象のアーティファクトを依存関係を解決しながらダウンロードするツールです。
> marun install com.example:example:1.0.1
利点
- Mavenリポジトリからデプロイできる
- したがってバージョン管理もいい感じにできる
欠点
- 通常はプライベートリポジトリが必要
gradleやmavenは依存関係を解決できますが、ビルドツールなだけのことはあって豪華ですし、依存関係のライブラリ(アーティファクト)の依存関係を解決してよしなに取り計らってくれる機能はありますが、ターゲットのアーティファクトのみをダウンロードするという機能はありません。Mavenリポジトリで依存関係を解決するということだけに特化したツールとしてApach Ivyというツールがありますが、これもApache Antと組み合わせてビルド用に使うことを主に考えられていますし、Ivy単体で利用するにはIvy.xmlを書かないと使えません。
なもので、Apache Ivyを使って、ターゲットとするアーティファクトとその依存関係のみをダウンロードするツールを作ってみました。
プライベートリポジトリが必要ですが、今は例えば、maven-publishプラグインやaws-mavenプラグインを使うと、簡単にAmazon S3上にMavenリポジトリを構築できます。
インストール
最初に使いにくいのはよくないので、pipでインストールするようになっています。コマンドライン部分はなるべくシステムPythonで動くように、Python2です。
> sudo pip install marun
init
とりあえず初期設定します。実行にはJavaが必要なので、先にインストールしておいてください。
> sudo marun init
configuration file is not found!
Your Maven Repository URL []: s3://your_repository
S3 Access Key []:(アクセスキーを入力します)
S3 Secret Key []:(シークレットキーを入力します)
s3の他、http/httpsでも使えます。入力すると設定ファイル*/etc/marun.conf*ができます。
ついでに、Ivyが頑張ってこんな感じでmarun自体の依存関係をダウンロードしてくれます。Amazon S3の関係で、jarファイルの依存は多めです。/var/lib/marunにぶち込みます。
Download: 130982 / 130982
Download: 1282424 / 1282424
Download: 241622 / 241622
:: loading settings :: url = jar:file:/var/lib/marun/lib/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
:: resolving dependencies :: caller#all-caller;working
confs: [runtime]
found jp.cccis.marun#marun;0.1.1 in maven.cccis.jp.s3.amazonaws.com
found com.amazonaws#aws-java-sdk-s3;1.11.475 in bintray/jcenter
[1.11.475] com.amazonaws#aws-java-sdk-s3;1.11.+
found com.amazonaws#aws-java-sdk-kms;1.11.475 in bintray/jcenter
found com.amazonaws#aws-java-sdk-core;1.11.475 in bintray/jcenter
found commons-logging#commons-logging;1.1.3 in bintray/jcenter
found org.apache.httpcomponents#httpclient;4.5.5 in bintray/jcenter
found org.apache.httpcomponents#httpcore;4.4.9 in bintray/jcenter
found commons-codec#commons-codec;1.10 in bintray/jcenter
found software.amazon.ion#ion-java;1.0.2 in bintray/jcenter
found com.fasterxml.jackson.core#jackson-databind;2.6.7.2 in bintray/jcenter
...
commons-logging#commons-logging;1.1.3 from bintray/jcenter in [runtime]
joda-time#joda-time;2.8.1 from bintray/jcenter in [runtime]
jp.cccis.marun#marun;0.1.1 from maven.cccis.jp.s3.amazonaws.com in [runtime]
org.apache.httpcomponents#httpclient;4.5.5 from bintray/jcenter in [runtime]
org.apache.httpcomponents#httpcore;4.4.9 from bintray/jcenter in [runtime]
software.amazon.ion#ion-java;1.0.2 from bintray/jcenter in [runtime]
:: evicted modules:
commons-logging#commons-logging;1.2 by [commons-logging#commons-logging;1.1.3] in [runtime]
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| runtime | 16 | 2 | 0 | 1 || 17 | 1 |
---------------------------------------------------------------------
使ってみる
プライベートリポジトリの何かをダウンロードして実行するのがいいのですが、ここはGoogle Closure Compilerをダウンロードしてみます。
> sudo marun install com.google.javascript:closure-compiler:+
:: loading settings :: url = jar:file:/var/lib/marun/lib/ivy-2.4.0.jar!/org/apache/ivy/core/settings/ivysettings.xml
:: resolving dependencies :: caller#all-caller;working
confs: [runtime]
found com.google.javascript#closure-compiler;v20181210 in repo1.maven.org
[v20181210] com.google.javascript#closure-compiler;+
found com.google.javascript#closure-compiler-externs;v20181028 in bintray/jcenter
found args4j#args4j;2.0.26 in bintray/jcenter
found com.google.errorprone#error_prone_annotations;2.3.1 in bintray/jcenter
found com.google.guava#guava;25.1-jre in bintray/jcenter
found org.checkerframework#checker-qual;2.0.0 in bintray/jcenter
found com.google.j2objc#j2objc-annotations;1.1 in bintray/jcenter
found org.codehaus.mojo#animal-sniffer-annotations;1.14 in bintray/jcenter
found com.google.protobuf#protobuf-java;3.0.2 in bintray/jcenter
found com.google.code.gson#gson;2.7 in bintray/jcenter
found com.google.code.findbugs#jsr305;3.0.1 in bintray/jcenter
found com.google.jsinterop#jsinterop-annotations;1.0.0 in bintray/jcenter
found com.google.auto.value#auto-value;1.4.1 in bintray/jcenter
found org.apache.ant#ant;1.9.7 in bintray/jcenter
downloading https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20181210/closure
...
---------------------------------------------------------------------
| | modules || artifacts |
| conf | number| search|dwnlded|evicted|| number|dwnlded|
---------------------------------------------------------------------
| runtime | 16 | 14 | 14 | 2 || 16 | 16 |
---------------------------------------------------------------------
こんな感じになります。
> ls -l
total 8
drwxr-xr-x 1 root root 512 12月 23 21:37 lib
-rw-r--r-- 1 root root 4438 12月 23 21:37 marun.json
> ls -l lib
total 18980
-rw-r--r-- 2 root root 3482 2月 26 2015 animal-sniffer-annotations-1.14.jar
-rw-r--r-- 2 root root 2036195 4月 12 2016 ant-1.9.7.jar
-rw-r--r-- 2 root root 74703 11月 3 2013 args4j-2.0.26.jar
-rw-r--r-- 2 root root 1504726 4月 7 2017 auto-value-1.4.1.jar
-rw-r--r-- 2 root root 343222 5月 6 2016 checker-qual-2.0.0.jar
-rw-r--r-- 2 root root 189289 10月 31 04:55 closure-compiler-externs-v20181028.jar
-rw-r--r-- 2 root root 10248761 12月 13 03:35 closure-compiler-v20181210.jar
-rw-r--r-- 2 root root 13162 4月 21 2018 error_prone_annotations-2.3.1.jar
-rw-r--r-- 2 root root 231952 6月 15 2016 gson-2.7.jar
-rw-r--r-- 2 root root 2734339 5月 24 2018 guava-25.1-jre.jar
-rw-r--r-- 2 root root 8764 7月 27 2016 j2objc-annotations-1.1.jar
-rw-r--r-- 2 root root 4075 7月 29 2016 jsinterop-annotations-1.0.0.jar
-rw-r--r-- 2 root root 19943 10月 9 2015 jsr305-3.0.1.jar
-rw-r--r-- 2 root root 1304415 9月 7 2016 protobuf-java-3.0.2.jar
marun run
で実行もできます。Closure Compilerで実行するのは例えばcom.google.javascript.jscomp.CommandLineRunner
ですが、ちょっとだけ賢いので、省略した名前でmainを探すため、こんなこともできます。
> marun run CommandLineRunner
The compiler is waiting for input via stdin.
中身
表示メッセージの通り、ほぼApache Ivyです。依存関係を解決するだけなら自前で書くのもそれほど大変ではなさそうでしたが、落とし穴がありそうなのと、gradleが昔は使っていたというのが決め手でした。(今は独自で依存関係解決しているらしい)
PythonでIvyの起動に必要な最低限のjarファイルをダウンロードして、あとはIvyでIvy含めて再度依存関係解決を図っています。
なお、marun.jsonの中身はこんな感じで、ちょっとした解析データが入ってるだけです。
{
"1545568648": {
"mains": [
"com.google.javascript.jscomp.CommandLineRunner",
"com.google.javascript.jscomp.LinterMain",
"org.kohsuke.args4j.Starter",
"org.apache.tools.ant.Diagnostics",
"org.apache.tools.ant.taskdefs.KeySubst",
"org.apache.tools.ant.taskdefs.optional.ejb.IPlanetEjbc",
"org.apache.tools.ant.taskdefs.optional.jlink.jlink",
"org.apache.tools.ant.util.ProcessUtil"
],
"dependencies": {
"com.google.code.gson:gson": {
"cache": "/var/lib/marun/ivy/com.google.code.gson/gson/jars/gson-2.7.jar",
"name": "gson-2.7.jar",
"revision": "2.7"
},
...
},
"install": [
"com.google.javascript:closure-compiler:+"
]
},
"context": [
1545568648
]
}
jsonファイルの中身の通り、ライブラリバージョンの保存や実行の補助もしてくれますが、本質的にはlibに全部のjarをダウンロードするだけなので、java -cp lib/*
で実行できます。
おわり
というわけで、Javaのデプロイ方法に悩んだので、隙間を埋めるデプロイツールを作ってみた話でした。ハマるシチュエーションもあると思うので、よければ使ってみてください。完成度は今のところ期待しないでください。(もうちょっと頑張りたい)
また、こんなツール作らなくてももうあるよ、とか、こうしたらいいとかあればぜひご意見ください!