最近はWebアプリをDocker化してクラウド上に展開することが増えてきていると思います。それを受けてPlayFramework製のアプリをDocker化する記事もちらほら見かけます。一方で、それらの記事でSBT Native Packagerを使ってDocker化しているのをあまり見かけなかったので、この記事ではその紹介をします。
SBT Native Packager とは
(Playに限らず)Scala製のアプリケーションを様々なパッケージ形式に変換するプラグインです。jar はもちろん、rpm や msi などのインストーラー形式だったり、GraalVM によるネイティブ形式だったり、今回紹介する Docker 形式だったりに対応しています。
Playには、バージョンは多少古いですがこのプラグインを内蔵しており、Playアプリケーションにおいては追加のプラグインなしにDocker化を行うことができます。
Docker 化
早速Docker化をみてみましょう。ここでは scala-seed プロジェクトをDocker化してみます。作られたプロジェクトの build.sbt をこんな感じにします。
name := """test"""
organization := "com.example"
version := "1.0-SNAPSHOT"
lazy val root = (project in file(".")).enablePlugins(PlayScala)
.enablePlugins(DockerPlugin)
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11"
)
scalaVersion := "2.13.8"
libraryDependencies += guice
libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "5.0.0" % Test
// Adds additional packages into Twirl
//TwirlKeys.templateImports += "com.example.controllers._"
// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "com.example.binders._"
驚くべきことに、あなたはDockerのベースイメージを決めるだけで、Dockerfileを書かなくて良いのです。
さて、とりあえず起動してみる場合は、secret を追加しておきましょう。
# https://www.playframework.com/documentation/latest/Configuration
play.http.secret.key="QCY?tAnfk?aZ?iwrNwnxIlR6CTf:G3gf:90Latabg@5241AB`R5W:1uDFN];Ik@n"
では、起動してみます。
$ sbt Docker/publishLocal
...(省略)
[error] #14 naming to docker.io/library/test:1.0-SNAPSHOT done
...(省略)
$ docker run -it -p 9000:9000 test:1.0-SNAPSHOT
...(省略)
2022-08-05 23:02:53 INFO play.core.server.AkkaHttpServer Listening for HTTP on /0.0.0.0:9000
起動したっぽいですね。 http://localhost:9000 にアクセスしてみましょう。
Welcome to Play!
が見れましたか?これを前提に、以下では様々なTipsを紹介します。
どんなDockerfileが作成されるの?
Dockerfile 書いてない、書かなくて良いと言われたって、時には生成される Dockerfile をみてみたいものです。そんな時は Docker/stage を実行し、target/docker/stage/Dockerfile
をみてみましょう。
$ sbt Docker/stage
...(省略)
$ cat target/docker/stage/Dockerfile
標準でマルチステージビルドされていて、デーモンユーザー(demiourgos728)で起動しているのがわかります。デーモンユーザーは書き込み権限がないので、コンテナの中身をぶっ壊さないのが保証されてて良いですね。
./logs/application.log (No such file or directory)
が出る
上との関連ですが、デーモンユーザーはコンテナへの書き込み権限がないので、そもそも logs フォルダを作れないんですね。ログはコンテナの外に記録されるようにしましょう。
$ docker run -it -p 9000:9000 -v "$(pwd)/logs:/opt/docker/logs" test:1.0-SNAPSHOT
クラウドにあげる際も、似たような形でマウントすると思います。マウント先のデフォルトは /opt/docker/logs
と覚えておけば良いでしょう。
ビルド時に [1] There are no exposed ports for your docker image
が出る
サジェストにあるように、build.sbt に expose ports を設定してあげましょう。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11",
dockerExposedPorts ++= Seq(9000) // <- これ
)
これにより Dockerfile に EXPOSE 9000
が追加されます。
タイムゾーンを Asia/Tokyo
にしたい
dockerEnvVars
を使います。これ以外にも必要な環境変数を追加できます。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11",
dockerExposedPorts ++= Seq(9000),
dockerEnvVars := Map("TZ" -> "Asia/Tokyo") // <- これ
)
本番用のコンフィグファイルで起動したい。
エントリーポイントにて引数を追加します。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11",
dockerExposedPorts ++= Seq(9000),
dockerEnvVars := Map("TZ" -> "Asia/Tokyo"),
dockerEntrypoint := Seq("bin/test", s"-Dconfig.resource=${sys.env.getOrElse("ENV", "")}.conf") // <- これ
)
この例では conf/${ENV}.conf
ファイルが指定されます。他にも好きな引数を dockerEntrypoint
に追加できます。
カスタムエントリーポイントを使いたい
Playで作られるエントリーポイントではなく、最初にちょっとした設定を施してアプリを起動したい、あると思います。そんな時は dist/bin/<<エントリーポイント>>
を作成します。
#!/bin/bash
echo "custom entrypoint!"
bin/test
このファイルを dockerEntrypoint
とします。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11",
dockerExposedPorts ++= Seq(9000),
dockerEnvVars := Map("TZ" -> "Asia/Tokyo"),
dockerEntrypoint := Seq("bin/entrypoint.sh") // <- これ
)
Playの規約で、dist ディレクトリにあるファイルは全てパッケージの中にコピーされます。つまり dist/bin/entrypoint.sh
は(WORKDIR
が /opt/docker
の場合) /opt/docker/bin/entrypoint.sh
にコピーされます。次に、SBT Native Packagerの規約で /opt/docker/bin/*
は実行可能ファイルとしてマークされます。カレントディレクトリは /opt/docker
なので bin/プロジェクト名
でアプリが起動します。
クラウドへプッシュしたい
dockerRepository
, Docker / packageName
, Docker / version
を設定します。packageName
と version
は ThisBuild
と同じ値なら変更する必要はありません。 dockerRepository
は Option[String]
であることに注意します。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "eclipse-temurin:11",
dockerExposedPorts ++= Seq(9000),
dockerEnvVars := Map("TZ" -> "Asia/Tokyo"),
dockerEntrypoint := Seq("bin/entrypoint.sh"),
dockerRepository := Some(s"${sys.env.getOrElse("AWS_ACCOUNT", "")}.dkr.ecr.${sys.env.getOrElse("AWS_REGION", "ap-northeast-1")}.amazonaws.com") // <- これ
)
ログインが必要な場合、sbt Docker/publish
の前にログインしていればOKです。
$ aws ecr get-login-password --region "${AWS_REGION}" | docker login --username AWS --password-stdin "${AWS_ACCOUNT}".dkr.ecr."${AWS_REGION}".amazonaws.com
$ sbt Docker/publish
出てきた環境変数は適宜読み替えてください。
ベースイメージに Amazon Corretto を使用したい。
Amazon Corretto のデフォルトベースイメージが Amazon Linux 2 であるため、useradd
がなく、デーモンユーザーの追加ができずにビルドに失敗します。デーモンユーザーでの起動を諦めるか、なんとかして useradd
をインストールする必要があります。
...(省略)...
.settings(
Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
dockerBaseImage := "amazoncorretto:11",
dockerExposedPorts ++= Seq(9000),
dockerEnvVars := Map("TZ" -> "Asia/Tokyo"),
dockerEntrypoint := Seq("bin/entrypoint.sh"),
dockerRepository := Some(s"${sys.env.getOrElse("AWS_ACCOUNT", "")}.dkr.ecr.${sys.env.getOrElse("AWS_REGION", "ap-northeast-1")}.amazonaws.com"),
// ↓これ
dockerCommands := {
// install useradd
import com.typesafe.sbt.packager.docker._
dockerCommands.value.foldLeft[Seq[CmdLike]](Nil) { (commands, command) =>
commands ++ {
command match {
case Cmd("USER", "root") =>
Seq(
command,
Cmd(
"RUN",
"yum install -y shadow-utils && rm -rf /var/cache/yum/* && yum clean all"
)
)
case _ =>
Seq(command)
}
}
}
}
)
RUN yum install -y shadow-utils && rm -rf /var/cache/yum/* && yum clean all
を追加したいだけなのにこの記述量は萎えますね... Amazon Corretto は alpine ベースのイメージもありますが、そっちは ash だったりするので、AshScriptSupport が必要だったりします。ただ、 is_cygwin がなかったりするので、 ubuntsu や bullseye 的なベースイメージに Amazon Corretto をインストールしたカスタムイメージを独自に用意する方が良いかもしれません。
まとめ
Playは標準でSBT Native Packagerを同梱しているので、Docker化やそのイメージのプッシュは build.sbt だけで完結します。便利ですね!!
それではみなさん、良きPlay/Scala/Dockerライフを!