Blog
sbt で Docker イメージを作る
こんにちは、基盤開発グループの阿川です。
基盤開発グループは、アドテクスタジオに存在する様々なプロダクトが共通して抱える課題を解決するべく動いているチームです。そこでは Finagle をベースに Scala で社内向けサービスを実装することになったのですが、せっかくのゼロから構築するシステムなので Docker を全面的に採用することにしました。これまでも限定的に Docker を使うことはあったのですが、コンテナスケジューラーまで導入して Docker を使うのは初めてのことです。
そんな流れがあり sbt で Docker イメージをビルドする方法を調査してみました。
sbt の Docker 関連プラグイン
ざっくり調べてみたところ sbt の Docker 関連プラグインは以下の 2 つが代表的のようでした。
前者の sbt-docker は Dockerfile の生成とイメージのビルドだけを行うシンプルなプラグインです。ほぼ Dockerfile をそのまま記述する感覚でイメージを作れますが、アプリケーションのパッケージングは自力で定義するか、 sbt-assembly や sbt-native-packager などと連携する必要があります。
後者の sbt-native-packger は基本的には Scala アプリケーションのパッケージングを行い ZIP アーカイブや RPM パッケージを出力するプラグインですが、その出力形式の 1 つとして Docker イメージがあります。
作成したアプリケーションを実行可能な形式で配布するには何らかのパッケージングが必要になるので、それも一緒にやってくれる scala-native-packager プラグインから見ていきましょう。
example01. sbt-native-packager で最小限の Docker イメージを作る
今回のアプリケーションコードは Hello, world を出力するだけのシンプルなものですが、Docker イメージを作る、という観点からは十分かと思います。プロジェクトは GitHub の example01 に公開しているので、全体像はそちらを参照してください。
1 2 3 |
object Boot extends App { println("Hello, world.") } |
さて、アプリケーションが完成したところで、まずは sbt-native-packager プラグインが利用できるようにするため project/plugins.sbt
へ記述を追加します。
1 |
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.1") |
そして build.sbt
を記述していきます。私は最初からマルチプロジェクトスタイルで書くようにしています。
1 2 3 4 5 6 7 8 9 10 11 |
lazy val root = (project in file(".")) .enablePlugins(JavaAppPackaging, AshScriptPlugin, DockerPlugin) .settings( organization := "io.github.atty303", name := "example01", version := "1.0.0-SNAPSHOT", scalaVersion := "2.11.8", mainClass in (Compile, run) := Some("Boot"), dockerBaseImage := "java:8-jdk-alpine" ) |
sbt-native-packager の固有の設定は enablePlugins でプラグインを有効化していること、 dockerBaseImage を設定していることぐらいですね。
有効にしているプラグインですが、それぞれ以下の役割を持っています。
- JavaAppPackaging — コマンドラインアプリケーションとしてパッケージングする Archetype
- AshScriptPlugin — bash ではなく ash で動く起動スクリプトを生成する
- DockerPlugin — Docker イメージとしての出力を有効にする
dockerBaseImage は Dockerfile の FROM ディレクティブに設定する値となります。デフォルトの値もありますが、これを変更せずに使うことはあまりないと思いますので、最小限の設定に加えておきました。今回は Docker Hub 公式の java リポジトリから Alpine Linux ベースの OpenJDK イメージを選択しています。
以上でビルドの準備ができました。Docker イメージをビルドするには docker:publishLocal タスクを実行します。
1 2 3 4 |
$ sbt docker:publishLocal ... snip ... [info] Built image example01:1.0.0-SNAPSHOT [success] Total time: 1 s, completed 2016/06/09 21:45:51 |
どうやら無事ビルドできたようなので、作成されたイメージを確認します。
1 2 3 |
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE example01 1.0.0-SNAPSHOT 1dbe2f913f6f 25 seconds ago 157 MB |
プロジェクトの name へ設定した名前がイメージ名、version がタグになるようですね。
では、作成されたイメージを実行してみます。
1 2 |
$ docker run -it --rm example01:1.0.0-SNAPSHOT Hello, world. |
無事動きました!
作成されたイメージの中を覗いてみましょう。イメージの ENTRYPOINT はアプリケーションの起動スクリプトに設定されているので、 –entrypoint オプションでシェルを起動するように設定します。
1 2 3 4 5 6 7 8 9 10 |
$ docker run -it --rm --entrypoint=sh example01:1.0.0-SNAPSHOT /opt/docker $ find /opt/ /opt/ /opt/docker /opt/docker/bin /opt/docker/bin/example01.bat /opt/docker/bin/example01 /opt/docker/lib /opt/docker/lib/io.github.atty303.example01-1.0.0-SNAPSHOT.jar /opt/docker/lib/org.scala-lang.scala-library-2.11.8.jar |
/opt/docker にアプリケーションが配置されていることがわかります。/opt/docker/bin/example01 が JavaAppPackaging プラグインによって生成された起動スクリプトですね。
このイメージのビルドに使用された Dockerfile も確認してみます。プロジェクトの target/docker/stage ディレクトリがイメージのビルドコンテキストになっています。(この stage ディレクトリの生成は sbt の docker:stage タスクが担当しています)
1 2 3 4 5 6 7 8 9 10 11 |
$ find target/docker/stage target/docker/stage target/docker/stage/Dockerfile target/docker/stage/opt target/docker/stage/opt/docker target/docker/stage/opt/docker/bin target/docker/stage/opt/docker/bin/example01 target/docker/stage/opt/docker/bin/example01.bat target/docker/stage/opt/docker/lib target/docker/stage/opt/docker/lib/io.github.atty303.example01-1.0.0-SNAPSHOT.jar target/docker/stage/opt/docker/lib/org.scala-lang.scala-library-2.11.8.jar |
1 2 3 4 5 6 7 |
FROM java:8-jdk-alpine WORKDIR /opt/docker ADD opt /opt RUN ["chown", "-R", "daemon:daemon", "."] USER daemon ENTRYPOINT ["bin/example01"] CMD [] |
build.sbt には dockerBaseImage ぐらいしか設定していないですが、アプリケーションを動かすのには十分な Dockerfile が生成されています。より細かい設定は Docker Plugin のドキュメントと Universal Plugin のドキュメントを参照するとよいでしょう。
以上、 sbt-native-packger で Docker イメージを作る方法でした。普通の sbt のビルドにちょっと設定を加えるだけで Docker イメージを作成できるので、なかなかお手軽な方法ではないかと思います。
ついでなので、 sbt-docker で同じように Docker イメージを作る方法も紹介します。
example02. sbt-docker (+ sbt-native-packger) で最小限の Docker イメージを作る
今度は sbt-docker プラグインを使って、同じように Docker イメージを作ってみます。プロジェクトは同じく GitHub の example02 にあるので参照してください。
sbt-docker を利用するときの plugins.sbt の記述は以下の通りです。
1 |
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.1.1") |
build.sbt も書きましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
lazy val root = (project in file(".")) .enablePlugins(JavaAppPackaging, AshScriptPlugin, sbtdocker.DockerPlugin) .settings( organization := "io.github.atty303", name := "example02", version := "1.0.0-SNAPSHOT", scalaVersion := "2.11.8", mainClass in (Compile, run) := Some("Boot"), dockerfile in docker := { // sbt-native-packager の stage タスクによってアプリがステージングされたディレクトリ val stageDir: File = stage.value val targetDir = "/opt/docker" new Dockerfile { from("java:8-jdk-alpine") copy(stageDir, targetDir) entryPoint(s"$targetDir/bin/${executableScriptName.value}") } }) |
dockerfile in docker のところが sbt-docker の設定になります。 sbt-docker には Dockerfile のテンプレートが存在しないので、必ずスクラッチから記述する必要があります。基本的に Dockerfile のディレクティブと対応しているので、 Docker を使い慣れている方は sbt-docker のほうがとっつきやすいかもしれませんね。
今回はアプリケーションのパッケージングに sbt-native-packager を利用し、生成された stage ディレクトリをイメージ内に配置するようにしました。
イメージのビルドは docker タスクで行います。
1 2 3 4 |
$ sbt docker ... snip ... [info] Tagging image 65d89d14c638 with name: io.github.atty303/example02 [success] Total time: 5 s, completed 2016/06/10 14:24:34 |
生成されたイメージを見てみましょう。
1 2 3 |
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE io.github.atty303/example02 latest 65d89d14c638 34 seconds ago 151.3 MB |
デフォルトでイメージ名は “${organization}/${name}” 、タグは latest になるようです。
Dockerfile も見てみます。
1 2 3 4 5 6 7 8 9 10 11 |
$ find target/docker target/docker target/docker/0 target/docker/0/stage target/docker/0/stage/bin target/docker/0/stage/bin/example02 target/docker/0/stage/bin/example02.bat target/docker/0/stage/lib target/docker/0/stage/lib/io.github.atty303.example02-1.0.0-SNAPSHOT.jar target/docker/0/stage/lib/org.scala-lang.scala-library-2.11.8.jar target/docker/Dockerfile |
1 2 3 |
FROM java:8-jdk-alpine COPY 0/stage /opt/docker ENTRYPOINT ["\/opt\/docker\/bin\/example02"] |
build.sbt に記述した内容がそのまま Dockerfile になっていますね。
以上、 sbt-docker による Docker イメージの作成でした。
example03. entrypoint を独自スクリプトに変更する (sbt-native-packger)
Docker イメージからコンテナを起動するとき、アプリケーションの起動スクリプトを実行する前に他のスクリプトを実行したいことがあると思います。例えば docker-compose で他のコンテナのサービス(DBなど)が利用可能になるのを待ったり、Hadoop の設定を調整したり、といったケースですね。
そのためには、まず sbt-native-packger のパッケージングに独自スクリプトを追加しなければなりません。これは src/universal にパッケージへ追加したいファイルを配置することで実現できます。サンプルでは src/universal/bin/entrypoint.sh にスクリプトを配置しました。
そして build.sbt では Dockerfile の ENTRYPOINT を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 |
lazy val root = (project in file(".")) .enablePlugins(JavaAppPackaging, AshScriptPlugin, DockerPlugin) .settings( organization := "io.github.atty303", name := "example03", version := "1.0.0-SNAPSHOT", scalaVersion := "2.11.8", mainClass in (Compile, run) := Some("Boot"), dockerBaseImage := "java:8-jdk-alpine", dockerEntrypoint := Seq("/opt/docker/bin/entrypoint.sh") ) |
このイメージを実行すると、ちゃんと ENTRYPOINT に設定したスクリプトが実行されていることがわかります。
1 2 3 |
$ docker run -it --rm example03:1.0.0-SNAPSHOT configured Hello, world. |
example04. sbt-docker + sbt-assembly で Spark アプリケーションのイメージを作る
ここまでアプリケーションのパッケージングは全て sbt-native-packger を利用してきました。他の方法を取る例として Spark アプリケーションの Docker イメージをビルドする例を紹介します。(プロジェクトは example04 です)
Spark アプリケーションのビルドというと、 spark-submit に渡す fat Jar をビルドする、ということになります。その fat Jar のビルドに sbt-assembly を、Docker イメージのビルドに sbt-docker を利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
lazy val root = (project in file(".")) .enablePlugins(sbtdocker.DockerPlugin) .settings( organization := "io.github.atty303", name := "example04", version := "1.0.0-SNAPSHOT", scalaVersion := "2.11.8", mainClass in (Compile, run) := Some("SparkPi"), libraryDependencies ++= Seq( "org.apache.spark" %% "spark-core" % "1.6.1" % "provided"), assemblyJarName in assembly := "example04.jar", dockerfile in docker := { new Dockerfile { from("gettyimages/spark:1.6.1-hadoop-2.6") copy(assembly.value, "/opt") } }) |
ポイントは Dockerfile の COPY ディレクティブに assembly.value という形で assembly タスクの出力(fat Jar ファイル)を参照してイメージに追加しているところです。この参照によって docker タスクから assembly タスクへの依存も宣言されたことになり、 docker タスクを実行するとちゃんと assembly タスクも実行してくれます。
では、出来あがったイメージを実行してみましょう。
1 2 3 4 |
$ docker run -it --rm io.github.atty303/example04 spark-submit /opt/example04.jar 16/06/13 09:47:21 INFO spark.SparkContext: Running Spark version 1.6.1 ... snip ... Pi is roughly 3.14562 |
spark-submit の local モードで実行できました!
実際には、Spark の実行環境(YARN や Mesos)への接続設定をした Docker のベースイメージを作成しておき、それをベースにアプリケーションの fat Jar を追加したイメージをビルドする、という運用になるかと思います。spark-submit する側の Spark 環境も含めて Docker コンテナの中に押し込められることは、複数の Spark バージョンが混在する環境や Spark のアップグレードをするときに恩恵を受けられるのではないでしょうか。
まとめ
今回は sbt で Docker イメージを作成するプラグインを 2 つ紹介しましたが、まずは sbt-native-packager を使っておけば間違いないと思います。sbt-docker の使いどころとしては、例えば Spark アプリケーションのように sbt-native-packager の Archetype が存在しないパッケージングは自前で書くしかないので、余分な機能のない sbt-docker を使う、という感じになるでしょうか。
それでは良き Scala + Docker ライフを!
Author