GeekFactory

int128.hatenablog.com

GradleによるJVMアプリケーションのパッケージングと配布 #gadvent

G*Advent Calendar(Groovy,Grails,Gradle,Spock...) Advent Calendar 2014 - Qiitaの10日目です。

アプリケーションを公開する際、ユーザが使いやすい形でアプリケーションを配布することで、より多くのユーザに使ってもらえることが期待できます。また、アプリケーションをサービスとして公開する際にも、インフラにデプロイしやすい形でアプリケーションをリリースすることで、より早くユーザに提供することができます。どんなに優れたアプリケーションであっても、インストールや実行に面倒な手間がかかる場合は魅力が半減してしまいます。

JVMベースのアプリケーションを配布する際の課題

JVMベース(Java、Groovy、Scalaなど)のアプリケーションをユーザに配布するには以下の課題が考えられます。

まず、アプリケーションの実行に必要な java コマンドの引数が肥大化する傾向があります。依存ライブラリをクラスパスに列挙した上で、さらにヒープサイズやGCをチューニングする引数*1を列挙すると、コマンドラインがターミナル上で数行に渡ることもめずらしくありません。このように、ユーザがコマンドラインから java コマンドを実行するのはあまり現実的でないため、JVMを実行するシェルスクリプトやバッチファイルを同梱することが一般的です。

また、Javaの世界にはnpmやRubyGemsのようなコマンドラインツールを配布する仕組みがありません*2。そのため、JVMベースのアプリケーションを公開する場合は、最適な配布形態を自分で考える必要があります。

本稿では、JVMベースのアプリケーションを配布する形態として以下を挙げて、それぞれの形態についてGradleを用いた実現方法を説明します。

  1. Zipアーカイブ
  2. 実行可能なFatJar
  3. 実行可能なWar(Webアプリケーションの場合のみ)
  4. Dockerイメージ

1. Zipアーカイブを配布する

実行に必要なファイルをZipアーカイブにまとめて配布します。ユーザが簡単に実行できるように、JVMを起動するシェルスクリプトやバッチファイルを同梱することが一般的です。ただし、ユーザはZipアーカイブを適当な場所に展開する必要があります。

Gradleでアプリケーション一式のZipアーカイブを生成するには、applicationプラグインを使います。Zipアーカイブにはシェルスクリプトやバッチファイルが含まれます。シェルスクリプトやバッチファイルがJVMを実行する際のデフォルト引数も指定できます。ビルドスクリプトの例を以下に示します。

plugins {
  id 'java'    // Javaアプリケーションの場合
  id 'groovy'  // Groovyアプリケーションの場合
  id 'application'
}

// Zipのファイル名に付与するバージョン番号
version = '1.0.0'
// エントリポイントのクラス名
mainClassName = 'org.example.Main'
// JVMの引数(ヒープサイズなど)
applicationDefaultJvmArgs = ['-Xms512m', '-Xmx512m']

distZipタスクを実行すると、build/distributionsの中にZipが生成されます。

# Zipアーカイブを生成する
./gradlew distZip

# Zipアーカイブの中身を確認する
zipinfo build/distributions/example-1.0.0.zip

Zipアーカイブの中身は以下のようになっています。

  • example-1.0.0/bin/example - シェルスクリプトUnixでアプリケーションを実行する場合に使用)
  • example-1.0.0/bin/example.bat - バッチファイル(Windowsでアプリケーションを実行する場合に使用)
  • example-1.0.0/lib/example-1.0.0.jar - アプリケーションのJar
  • example-1.0.0/lib/*.jar - アプリケーションの依存ライブラリ

2. 実行可能なFatJarを配布する

実行に必要なファイルを1つのJarにまとめて配布します。これにより、ユーザは java -jar コマンドを叩くだけでアプリケーションを実行できるため、アーカイブの展開や配置といったインストールの手間が省けます。ただし、JVMのデフォルト引数はJarに組み込めないため、ユーザが指定する必要があります。

実行時の依存ライブラリを同梱したJarは、一般にFatJarなどと呼ばれます。GradleでFatJarを生成するにはShadowプラグインを使います。実行可能なFatJarを生成するビルドスクリプトを以下に示します。

plugins {
  id 'java'    // Javaアプリケーションの場合
  id 'groovy'  // Groovyアプリケーションの場合
  id 'com.github.johnrengelman.shadow' version '1.2.0'
}

version = '1.0.0'  // Jarのファイル名に付与するバージョン番号

shadowJar {
  manifest {
    // エントリポイントのクラスを指定する
    attributes 'Main-Class': 'org.example.Main'
  }
}

shadowJarタスクを実行すると、build/libsの中にFatJarが生成されます。

# FatJarを生成する
./gradlew shadowJar

# FatJarを実行してみる
java -jar build/libs/example-1.0.0-all.jar

依存関係のカスタマイズ

Shadowプラグインのデフォルト設定では、実行時の依存ライブラリ(dependenciesのcompileとruntimeに書いたもの)がFatJatに同梱されます。必要に応じて、FatJarに同梱するライブラリを変更できます。

例えば、ライブラリとして配布するJarとコマンドラインツールとして配布するJarで依存関係を変えたいことがあります。以下のビルドスクリプトでは、ライブラリとして配布するJarはSLF4Jに依存するように設定して、コマンドラインツールとして配布するJarにはLogbackを同梱するように設定しています。

configurations {
  // コマンドラインツール用のconfigurationを定義
  cliRuntime
}

dependencies {
  // ライブラリとして配布するJarはSLF4Jに依存
  compile 'org.slf4j:slf4j-api:1.7.7'
  // コマンドラインツールとして配布するJarはLogbackに依存
  cliRuntime 'ch.qos.logback:logback-classic:1.1.2'
}

shadowJar {
  // コマンドラインツールとして配布するJarにはLogbackを同梱
  configurations = [project.configurations.runtime, project.configurations.cliRuntime]
}

3. 実行可能なWarを配布する

WebアプリケーションをWarで配布する際にJettyやTomcatを同梱します。これにより、ユーザは java -jar コマンドを叩くだけでアプリケーションを実行できるため、APサーバにデプロイする手間が省けます。JenkinsやGitBucketは実行可能なWarを採用しており、ユーザがすぐにアプリケーションを実行できるように工夫されています。

実行可能なWarは、WebアプリケーションのWarと実行可能なFatJarを合体させた構造になっています。例えば、Jettyを組み込んだ実行可能なWarは以下の構造になります。

パス 内容
META-INF/MANIFEST.MF Jarのマニフェスト(エントリポイントに com.example.Main を指定)
com/example/Main.class Jettyを起動するクラス
org/eclipse/jetty/~ Jettyのパッケージ群
index.html Webアプリケーションの静的コンテンツ
WEB-INF/~.xml Webアプリケーションの設定群
WEB-INF/lib Webアプリケーションの依存ライブラリ

実行可能なWarを生成する手順はAPサーバやアプリケーションの構成に依存するため、ここでは省略します。

4. Dockerイメージを配布する

OS、JVM、アプリケーションが含まれるDockerイメージを配布します。これにより、ユーザは docker run コマンドを叩くだけでアプリケーションを実行できるため、JVMをインストールしたりアプリケーションを配置したりする手間が省けます。また、開発者はインストールの手順を細かく説明する必要がなくなります。

Dockerイメージを配布するにはDocker HubのAutomated Buildを使う方法が一般的です。Automated Buildを使うと、GitHubやBitbucketで公開しているアプリケーションをDocker Hubがビルドしてくれます。ユーザは docker pulldocker run でDocker Hubからイメージを取得できます。

コンテナでアプリケーションを実行するには以下の方法があります。

  1. イメージにアプリケーション一式を配置しておいて、コンテナでシェルスクリプトを実行する(前述1と組み合わせる方法)
  2. イメージに実行可能なFatJarを配置しておいて、コンテナでjava -jarを実行する(前述2と組み合わせる方法)

現状の配布形態に合わせてaとbを選択するとよいでしょう。特にメリットやデメリットはないと思います。

GradleとDockerを組み合わせる

ここでは、イメージにFatJarを配置する方法(上記b)を考えます。イメージのビルド時は、Gradleでアプリケーションをビルドして、生成されたFatJarを適当な場所に配置するようにします。コンテナの実行時は、配置済みのFatJarを実行します。

Dockerfileを書く際のポイントを以下に示します。

まず、イメージのサイズをできる限り小さくするため、Gradleの中間生成物(クラスファイルや依存ライブラリ)をイメージに書き込まないようにします。volume でディレクトリを指定すると、ビルド時にそのディレクトリにファイルを書き込んでもイメージに書き込まれないという性質*3を使います。

Dockerでは、コンテナを実行する時のコマンドを cmd または entrypoint で指定できます。ユーザがアプリケーションに引数を渡せるように entrypoint を使うと便利です。ただし、パフォーマンスチューニングなどでユーザがJVM引数を指定する必要性が高い場合は cmd を使うとよいでしょう。

Gradleは、デフォルトではbuild.gradleが存在するディレクトリの名前に基づいてJarの名前を決めます。そのため、ソースコードを配置するディレクトリの名前に注意します。もしくは、 settings.gradle でプロジェクトの名前を明示します。

Dockerfileの例を以下に示します。ベースイメージにはOracle JDKが用意されているdockerfile/javaを選択しましたが、他でも構いません。

from dockerfile/java:oracle-java7

volume /usr/src/example
copy . /usr/src/example
run cd /usr/src/example && \
    ./gradlew --gradle-user-home=.gradle shadowJar && \
    cp -a build/libs/example-1.0.0-all.jar /

entrypoint ["java", "-Xms512m", "-Xmx512m", "-jar", "/example-1.0.0-all.jar"]

イメージのビルドとコンテナの実行は以下のコマンドを使います。

# イメージをビルドする
docker build -t int128/example .

# コンテナを実行する
docker run --rm int128/example アプリケーションに渡す引数...

まとめ

Apache Tomcatをはじめとする多くのオープンソースソフトウェアは、伝統的にZipやTarのアーカイブで配布されています。一方で、JenkinsやGitBucketなどのように、実行可能なJarやWarで配布されているソフトウェアもあります。最近では、Dockerイメージを公開しているソフトウェアが増えています。

ユーザの視点で考えるとコマンドラインからすぐに実行できるJarやWarが使いやすいため、まずは実行可能なJarやWarを検討するとよいでしょう。ただし、JVMを実行するシェルスクリプトや設定ファイル群を合わせて配布したい場合はZipアーカイブが最適な選択となります。今後、Dockerがより一般的になると、ユーザはJVMのインストールを意識せずにアプリケーションを使えるようになるでしょう。

本稿では取り上げませんでしたが、IntelliJ IDEAのようにネイティブのインストーラを提供する形態もあります。デスクトップアプリケーションの場合は魅力的な選択肢になるでしょう。

明日は、id:kyon_mmさんによるGeb 0.9.xの新機能に関する記事です。

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

Gradle徹底入門 次世代ビルドツールによる自動化基盤の構築

*1:少し前のRebuild.fmでも話題になっていましたが、JVMのデフォルト設定はいけてないですよね

*2:もし知っていたら教えてください。ライブラリの配布であればMaven Centralがあります

*3:これがDockerの仕様通りなのかは未確認です