LoginSignup
3
2

More than 1 year has passed since last update.

PlayFrameworkアプリをDocker化する

Last updated at Posted at 2022-08-06

最近は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 をこんな感じにします。

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 を追加しておきましょう。

conf/application.conf
# 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 を設定してあげましょう。

build.sbt
  ...(省略...
  .settings(
    Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
    dockerBaseImage := "eclipse-temurin:11",
    dockerExposedPorts ++= Seq(9000)  // <- これ
  )

これにより Dockerfile に EXPOSE 9000 が追加されます。

タイムゾーンを Asia/Tokyo にしたい

dockerEnvVars を使います。これ以外にも必要な環境変数を追加できます。

build.sbt
  ...(省略...
  .settings(
    Universal / javaOptions ++= Seq("-Dpidfile.path=/dev/null"),
    dockerBaseImage := "eclipse-temurin:11",
    dockerExposedPorts ++= Seq(9000),
    dockerEnvVars := Map("TZ" -> "Asia/Tokyo") // <- これ
  )

本番用のコンフィグファイルで起動したい。

エントリーポイントにて引数を追加します。

build.sbt
  ...(省略...
  .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/<<エントリーポイント>> を作成します。

dist/bin/entrypoint.sh
#!/bin/bash

echo "custom entrypoint!"

bin/test

このファイルを dockerEntrypoint とします。

build.sbt
  ...(省略...
  .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 を設定します。packageNameversionThisBuild と同じ値なら変更する必要はありません。 dockerRepositoryOption[String] であることに注意します。

build.sbt
  ...(省略...
  .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 をインストールする必要があります。

build.sbt
  ...(省略...
  .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ライフを!

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2