blog.petitviolet.net

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

2018-02-09

QiitaScalasbtDocker

この記事はなに

タイトル通り。 主に 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をすれば見ることが可能。

from: https://qiita.com/petitviolet/items/cf5d699521b08e6ec933