この記事はなに
タイトル通り。
主にsbt-native-packagerのとりあえずの使い方紹介。
Akka-HTTPでWebアプリを実装し、sbt-native-packagerを使ってDockerイメージを作成、localhostで稼働させてHTTPリクエストを受け付けられるようにするまで。
環境
- Scala 2.12.4
- sbt 1.0.4
- Akka-HTTP 10.0.10
- sbt-native-packager 1.3.3
ScalaのWebアプリ
build.sbtにはAkka-HTTPの依存を追加しておく。
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.10",
続いて動かすmainクラスの実装。
実装自体は何でもよいので、シンプルに。
import akka.actor.ActorSystem
import akka.event.Logging
import akka.http.scaladsl.Http
import akka.http.scaladsl.server._
import akka.stream.ActorMaterializer
import scala.concurrent.Await
import scala.concurrent.duration.Duration
object main extends App with Directives {
implicit val system: ActorSystem = ActorSystem("my-sample-app")
implicit val materializer: ActorMaterializer = ActorMaterializer()
// GET /indexでリクエストのURLパラメータとUserAgentを返却する
val route: Route =
(get & pathPrefix("index") & extractUri & headerValueByName("User-Agent")) {
(uri, ua) =>
logRequestResult("/index", Logging.InfoLevel) {
complete(s"param: ${uri.query().toMap}, user-agent: ${ua}}")
}
}
val host = sys.props.get("http.host") getOrElse "0.0.0.0"
val port = sys.props.get("http.port").fold(8080) { _.toInt }
val f = Http().bindAndHandle(route, host, port)
println(s"server at [$host:$port]")
Await.ready(f, Duration.Inf)
}
Akka-HTTPのHttp().bindAndHandle
した結果をAwait.ready
で無限に待つ。
簡単に動かすだけならこれでよい。
sbt run
すれば起動するはず。
build.sbtにdockerizeの設定を書く
まずはproject/plugins.sbtに以下を書く
// https://github.com/sbt/sbt-native-packager
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3")
続いてbuild.sbtに以下のように設定を記述する
enablePlugins(JavaAppPackaging)
// enablePlugins(JavaServerAppPackaging) // どっちでも動く
// Dockerfileに書く内容
packageName in Docker := "sample-webapp"
version in Docker := "1.0"
dockerRepository := Some("petitviolet")
maintainer in Docker := "petitviolet <mail@example.com>"
dockerExposedPorts := List(8080)
dockerBaseImage := "openjdk:latest"
dockerCmd := Nil
Dockerfileを書いたことがあれば何となくわかるはず。
in Docker
って書くかどうかが若干難しいが、dockerXxx
なら不要でそうじゃないなら必要、くらいでよいはず(間違ってたら指摘して欲しいです)。
これでpetitviolet/sample-webapp:1.0
としてイメージを作ることが出来る。
Dockerイメージを作る
sbtでコマンドを実行するだけで良い
sbt 'project main' 'docker:publishLocal'
docker:publishLocal
でdocker buildしてくれる。
結果を見るにはdocker images
すればいい。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
petitviolet/sample-webapp 1.0 5032e8dc5fa9 1 minutes ago 800MB
ちなみにこの際にdocker build
で使用するDockerfileがtarget/docker/stage
配下に生成されていて、中を見るとこうなっている。
FROM openjdk:latest
LABEL MAINTAINER="petitviolet <mail@example.com>"
WORKDIR /opt/docker
ADD opt /opt
RUN ["chown", "-R", "daemon:daemon", "."]
EXPOSE 8080
USER daemon
ENTRYPOINT ["bin/main"]
CMD []
ADD
するoptディレクトリ内には起動用のshellスクリプト(bin/main)と、依存ライブラリ(lib)が入っている。
ENTRYPOINTについて
実行可能なmainクラスが1つしかなければ、target/docker/stage/opt/docker/bin/main
に実行ファイルが置かれる。
そのためENTRYPOINT ["bin/main"]
となっていて動くようになっている。
複数のmainクラスが存在した場合、たとえばnet.petitviolet.MainServer
とnet.petitviolet.MainClient
みたいにあった場合、
target/docker/stage/opt/docker/bin/
配下にmain-server
とmain-client
という2つの実行ファイルが配置される。
しかし、ENTRYPOINT
はbin/main
を動かそうとするため、docker run
するとコケてしまう。
これに対する解決策は2つある。
- mainクラスを指定する
-
mainClass in Compile := Some("net.petitviolet.MainServer")
とすれば、bin/main
で実行可能
-
-
ENTRYPOINT
を指定する-
dockerEntryPoint := List("bin/main-server")
として実行するファイルを決め打ちする
-
前者の方が実行ファイル名に意識を向けなくてもよいため楽でいいが、ローカルで複数のmainクラスを動かしたい時にrunMain
しないといけなくて面倒になる。
一方で後者は実行ファイル名に意識を向ける必要があってちょっと歪になってしまうかも。
mainClass
の指定もdockerEntryPoint
の指定も、文字列でミスっていた場合にはdocker run
するまでわからないため、ミスの怖さは同等。
ローカルで起動する
普通にdocker run -d
すればデーモン起動する。
$ docker run -d -p 8080:8080 --name sample petitviolet/sample-webapp:1.0
9fc179ccfc37b654d766b1eeda944e5f17e29d6b1d51ea08c2322eabc873ba8b
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9fc179ccfc37 petitviolet/sample-webapp:1.0 "bin/main" 7 seconds ago Up 1 second 0.0.0.0:8080->8080/tcp sample
リクエストを送ってみる。
$ curl 'localhost:8080/index?key=value' -A hoge
param: Map(key -> value), user-agent: hoge}
普通に標準出力に垂れ流しているアプリケーションログはdocker logs
をすれば見ることが可能。