Posted at

Scalaで作ったWebアプリをDockerizeして動かす

More than 1 year has passed since last update.


この記事はなに

タイトル通り。

主にsbt-native-packagerのとりあえずの使い方紹介。


Akka-HTTPでWebアプリを実装し、sbt-native-packagerを使ってDockerイメージを作成、localhostで稼働させてHTTPリクエストを受け付けられるようにするまで。


環境


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.MainServernet.petitviolet.MainClientみたいにあった場合、

target/docker/stage/opt/docker/bin/配下にmain-servermain-clientという2つの実行ファイルが配置される。


しかし、ENTRYPOINTbin/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をすれば見ることが可能。