petitviolet blog

    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