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

## この記事はなに

タイトル通り。
主にsbt-native-packagerのとりあえずの使い方紹介。  
Akka-HTTPでWebアプリを実装し、sbt-native-packagerを使ってDockerイメージを作成、localhostで稼働させてHTTPリクエストを受け付けられるようにするまで。

## 環境

- Scala 2.12.4
- sbt 1.0.4
- [Akka-HTTP](https://doc.akka.io/docs/akka-http/current/) 10.0.10
- [sbt-native-packager](http://www.scala-sbt.org/sbt-native-packager/) 1.3.3

## ScalaのWebアプリ

build.sbtにはAkka-HTTPの依存を追加しておく。

```scala
libraryDependencies += "com.typesafe.akka" %% "akka-http" % "10.0.10",
```

続いて動かすmainクラスの実装。
実装自体は何でもよいので、シンプルに。

```scala
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に以下を書く

```scala
// https://github.com/sbt/sbt-native-packager
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3")
```

続いてbuild.sbtに以下のように設定を記述する

```scala
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`すればいい。

```shell-session
$ 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`すればデーモン起動する。

```shell-session
$ 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
```

リクエストを送ってみる。

```shell-session
$ curl 'localhost:8080/index?key=value' -A hoge
param: Map(key -> value), user-agent: hoge}
```

普通に標準出力に垂れ流しているアプリケーションログは`docker logs`をすれば見ることが可能。