Kotlinで書いたAPIサーバをDockerコンテナで運用する場合、どのような方式が実用的かを考えてみた。パターンと言っても今のとこ今回の一種くらい。
サンプルアプリ
今回書いたサンプルはこちら。
以前書いたSpark Framework with Kotlinの延長線上で、今回もSpark FrameworkをKotlinで書いてます。
エンドポイントはこれだけ。
package io.stormcat import spark.Spark.* fun main(args: Array<String>) { get("/echo", { req, res -> "Hello, ${req.queryParams("name")}!" }) }
GradleでJarをビルドする
KotlinなのでもちろんGradleを使ってビルドします。重要なのは、作成したアプリケーションをSpringBootのように一つのJARファイルにしてしまう、javaコマンドに-jarを渡すだけで実行できるようにすることです。雑にベタッと貼りましょう。
group 'io.stormcat' buildscript { ext.kotlin_version = '1.0.4' repositories { mavenCentral() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } apply plugin: 'java' apply plugin: 'kotlin' apply plugin: "idea" apply plugin: 'application' mainClassName = "io.stormcat.ApplicationKt" processResources.destinationDir = compileJava.destinationDir compileJava.dependsOn processResources kapt { generateStubs = false } idea { module { inheritOutputDirs = false outputDir = file("$buildDir/classes/main/") } } sourceCompatibility = 1.8 targetCompatibility = 1.8 repositories { jcenter() maven { url "http://repository.jetbrains.com/all" } mavenCentral() } dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" compile "com.sparkjava:spark-core:2.5.4" testCompile "junit:junit:4.11" } jar { exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' manifest { attributes "Main-Class" : "io.stormcat.ApplicationKt" } from configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } sourceSets { main.kotlin.srcDirs += 'src/main/kotlin' main.java.srcDirs += 'src/main/java' }
ポイントは
- applicationプラグインを使う
- アプリケーションを実行するためのmainクラスを指定する。mainメソッドはapplication.ktに作っているので、Javaのクラス上mainクラスはApplicationKtになる
- jarに何か証明書が紛れ込んでアプリケーションの起動に失敗するので、RSA/SF/DSA拡張子のファイルをマニフェスト領域から除去する
Dockerfileを作る
Alpine Linux
一般的にJavaアプリケーションを作ると、様々のライブラリに依存するためそこそこサイズが大きくなります。ここで利用するのは軽量なディストリビューションでお馴染みのAlpine LinuxベースのDockerイメージです。AlpineをDockerイメージに使う手法は昨年一気にデファクトになった感あるので、知らない方はググってみてください。
ちなみにDockerの各種公式イメージもAlpine化がどんどん進んできていて、JavaのイメージもAlpineベースのものが用意されてます。
サイズは約50MBで、AlpineじゃないJavaのイメージは260MBとかあるのでかなりスリムになっているのがわかります。今回はjava:openjdk-8-jdk-alpineを利用。
Dockerfile内でビルド
このベースイメージを使ってこのサンプルアプリのDockerイメージを作ります。こんな感じに。
FROM java:openjdk-8-jdk-alpine COPY . /spark-kotlin-docker RUN apk update && \ apk add --virtual build-dependencies build-base bash curl && \ cd /spark-kotlin-docker && ./gradlew clean && \ cd /spark-kotlin-docker && ./gradlew build && \ mkdir -p /usr/local/spark-kotlin-docker/lib && \ cp -R /spark-kotlin-docker/build/libs/* /usr/local/spark-kotlin-docker/lib/ && \ apk del build-dependencies && \ rm -rf /var/cache/apk/* && \ rm -rf ~/.gradle && \ rm -rf /spark-kotlin-docker
- リポジトリをガサッとdockerコンテナ内にコピー(.dockerignoreを使って、build/や.git/をコピーしないということもできる)
- AlpineのパッケージマネージャAPKのリポジトリをアップデート
- ビルドに必要なbuild-baseやbash(gradlewがbashに依存しているため)、curl等をインストール
- gradleでビルドしてJarを作り、適当な場所に置く
- ビルドで利用した産業廃棄物を削除
これが基本的なビルドプロセス。RUNを1回で数珠つなぎにしてるのは、RUNの度に生成されるDockerイメージのレイヤーを少しでも減らすため。
jolokia
運用を考えて、ちゃんとJVMのメトリクスを取れるようにしておきます。ここで選択するのは安定のjolokia。
jolokiaはDockerfile内でダウンロードするようにします。最初にAPKでcurlをインストールしたのがここで活きます。
RUN apk update && \ apk add --virtual build-dependencies build-base bash curl && \ cd /spark-kotlin-docker && ./gradlew clean && \ cd /spark-kotlin-docker && ./gradlew build && \ mkdir -p /usr/local/spark-kotlin-docker/lib && \ cp -R /spark-kotlin-docker/build/libs/* /usr/local/spark-kotlin-docker/lib/ && \ curl -o /usr/local/spark-kotlin-docker/lib/jolokia-jvm-agent.jar https://repo1.maven.org/maven2/org/jolokia/jolokia-jvm/1.3.5/jolokia-jvm-1.3.5-agent.jar && \ apk del build-dependencies && \ rm -rf /var/cache/apk/* && \ rm -rf ~/.gradle && \ rm -rf /spark-kotlin-docker
ENTRYPOINTでアプリケーションを起動する処理を記述しますが、ついでに-javaagentオプションを設定できるようにしておいてjolokiaを有効にします。ポート8778にHTTPリクエストを投げるとメトリクスを取得できます。
ENTRYPOINT java $JAVA_OPTS -javaagent:/usr/local/spark-kotlin-docker/lib/jolokia-jvm-agent.jar=port=8778,host=0.0.0.0 -jar /usr/local/spark-kotlin-docker/lib/spark-kotlin-docker.jar
最後に、ポートをバインドしておわり。4567はSparkのデフォルトポート、8778はjolokia。
FROM java:openjdk-8-jdk-alpine COPY . /spark-kotlin-docker RUN apk update && \ apk add --virtual build-dependencies build-base bash curl && \ cd /spark-kotlin-docker && ./gradlew clean && \ cd /spark-kotlin-docker && ./gradlew build && \ mkdir -p /usr/local/spark-kotlin-docker/lib && \ cp -R /spark-kotlin-docker/build/libs/* /usr/local/spark-kotlin-docker/lib/ && \ curl -o /usr/local/spark-kotlin-docker/lib/jolokia-jvm-agent.jar https://repo1.maven.org/maven2/org/jolokia/jolokia-jvm/1.3.5/jolokia-jvm-1.3.5-agent.jar && \ apk del build-dependencies && \ rm -rf /var/cache/apk/* && \ rm -rf ~/.gradle && \ rm -rf /spark-kotlin-docker ENTRYPOINT java $JAVA_OPTS -javaagent:/usr/local/spark-kotlin-docker/lib/jolokia-jvm-agent.jar=port=8778,host=0.0.0.0 -jar /usr/local/spark-kotlin-docker/lib/spark-kotlin-docker.jar EXPOSE 4567 8778
Dockerビルドして動かしてみる。
ビルドして、コンテナ起動。
$ docker build -t stormcat24/spark-kotlin-docker . $ docker run -d -p 4567:4567 -p 8778:8778 stormcat24/spark-kotlin-docker
アプリケーションのエンドポイントにリクエストを投げる。
$ curl -s http://localhost:4567/echo?name=nekotan Hello, nekotan!%
jolokiaのメトリクスはこんなふうに取れる。
$ curl -s http://localhost:8778/jolokia/read/java.lang:type=Memory | jq . { "request": { "mbean": "java.lang:type=Memory", "type": "read" }, "value": { "ObjectPendingFinalizationCount": 0, "Verbose": false, "HeapMemoryUsage": { "init": 33554432, "committed": 32505856, "max": 466092032, "used": 4764040 }, "NonHeapMemoryUsage": { "init": 2555904, "committed": 15204352, "max": -1, "used": 14577264 }, "ObjectName": { "objectName": "java.lang:type=Memory" } }, "timestamp": 1483267842, "status": 200 }
Dockerイメージのサイズ
サンプルアプリにほとんど機能がないため、依存してるのはKotlinとSparkだが約160MBになった。
$ docker images | grep spark-kotlin-docker stormcat24/spark-kotlin-docker latest 44f28ceaa7b9 40 minutes ago 162.2 MB
色々機能を追加していって依存が増えれば、200-250MB近辺くらいでしょうか。JVM系言語を使うとある程度大きくなるのは避けられないですが、JVM系でこれくらいにおさえられるならまあ及第点かなという印象。
まとめ
まあまあいいんじゃないの。