Javaの新しいデプロイツールを作ってみた

# 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を定義するので、次のように実行できます。

```sh
> java -jar xxxx.jar arg1 arg2
```

最近(?)はuber jarやshadow jarという言い方もあるみたいですね。

### 利点
* 単一ファイルで完結していて安全で配布しやすい
* mainを定義できる

### 欠点
* だいたい実際にfatになる。数十MBサイズ
* mainを一つしか持てない
    * main定義以外のmainを起動するときは起動方法を変えることになる

これはよく見かける手法です。単体ファイルを転送すればそれで済むというのがいいところです。全部入りなので、部分的に更新されてひどいことになるという心配もありませんし、mainクラスを忘れてしまう心配もありません。

個人的にはファットすぎる（クソでかい）のと、実動環境だけを見たときには、使用ライブラリや実行しているmainクラスの名前がすぐにわからず、解凍してみないとなんとも言えないので、サーバで動かすときにはあんまり好みに合いません。たぶん、ちゃんとした命名規則なりのルールの下で運用するといいと思うんですが。

gradleとかで、依存関係の.jarファイルをかき集めるのが面倒くさいという理由でfat jarを選ばれることもあって、ちょっと悲しいですね。それほど一般的ではないようですが、依存関係ライブラリは[こういう手法](https://stackoverflow.com/questions/23109276/gradle-task-to-put-jars-from-maven-repository-into-project-lib-folder)で集められます。

## warファイルにまとめる
Web以外ではあまり見かけないのですが、warファイルやearファイルでもfat jarと同じことができます。warの方がアプリケーション設定ファイルを含めつつクラスパスからは除外するなど、細かい制御ができるので、向いているかもしれません。

### 利点
* fat jarっぽいことも、zipっぽいこともできる

### 欠点
* 設定ファイルを現地で書き換えた後でバイナリだけアップデートしにくい

Java EE環境が多いならwarファイルは便利だと思います。warファイルなら多少はファイルサイズが大きくても許されるのではないでしょうか（偏見）

## Dockerイメージ
Dockerコンテナ内に実行環境を生成してイメージを作成します。JavaのDockerイメージなんて重い？いいえ、Alphine Linux/musc libc/Jigsawでまあまあコンパクトになります。

* [Dockerで動かす軽量なJava環境の作成](https://qiita.com/zgmf_mbfp03/items/e86dd1f03f96d4aa5dd5)

最近では[Google/Jib](https://github.com/GoogleContainerTools/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
https://github.com/nishemon/marun

Mavenリポジトリから対象のアーティファクトを依存関係を解決しながらダウンロードするツールです。

```sh
> marun install com.example:example:1.0.1
```

### 利点
* Mavenリポジトリからデプロイできる
    * したがってバージョン管理もいい感じにできる

### 欠点
* 通常はプライベートリポジトリが必要

gradleやmavenは依存関係を解決できますが、ビルドツールなだけのことはあって豪華ですし、依存関係のライブラリ（アーティファクト）の依存関係を解決してよしなに取り計らってくれる機能はありますが、ターゲットのアーティファクトのみをダウンロードするという機能はありません。Mavenリポジトリで依存関係を解決するということだけに特化したツールとして[Apach Ivy](http://ant.apache.org/ivy/)というツールがありますが、これもApache Antと組み合わせてビルド用に使うことを主に考えられていますし、Ivy単体で利用するにはIvy.xmlを書かないと使えません。

なもので、Apache Ivyを使って、ターゲットとするアーティファクトとその依存関係のみをダウンロードするツールを作ってみました。

プライベートリポジトリが必要ですが、今は例えば、[maven-publishプラグイン](https://qiita.com/disc99/items/8297b08d0fa1081649bb)や[aws-mavenプラグイン](https://qiita.com/suzutt/items/bf1e8a8e425a9077b96c)を使うと、簡単にAmazon S3上にMavenリポジトリを構築できます。

## インストール
最初に使いにくいのはよくないので、pipでインストールするようになっています。コマンドライン部分はなるべくシステムPythonで動くように、Python2です。

```sh
> sudo pip install marun
```

### init
とりあえず初期設定します。実行にはJavaが必要なので、先にインストールしておいてください。

```sh
> 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*にぶち込みます。

```sh:Ivyが頑張る
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](https://github.com/google/closure-compiler)をダウンロードしてみます。

```sh
> 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  |
	---------------------------------------------------------------------
```

こんな感じになります。

```sh
> 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を探すため、こんなこともできます。

```sh
> marun run CommandLineRunner
The compiler is waiting for input via stdin.
```

## 中身
表示メッセージの通り、ほぼ[Apache Ivy](http://ant.apache.org/ivy/)です。依存関係を解決するだけなら自前で書くのもそれほど大変ではなさそうでしたが、落とし穴がありそうなのと、gradleが昔は使っていたというのが決め手でした。（今は独自で依存関係解決しているらしい）

PythonでIvyの起動に必要な最低限のjarファイルをダウンロードして、あとはIvyでIvy含めて再度依存関係解決を図っています。

なお、marun.jsonの中身はこんな感じで、ちょっとした解析データが入ってるだけです。

```json: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のデプロイ方法に悩んだので、隙間を埋めるデプロイツールを作ってみた話でした。ハマるシチュエーションもあると思うので、よければ使ってみてください。完成度は今のところ期待しないでください。（もうちょっと頑張りたい）

また、こんなツール作らなくてももうあるよ、とか、こうしたらいいとかあればぜひご意見ください！