2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Scala で作った Web アプリを Dockerize して動かす(令和 6 年最新版)

Last updated at Posted at 2021-03-06

既存の Java アプリをコンテナ化するにあたり「JVM でマイクロサービスといえば Scala と Akka-HTTP だよな~」という気持ちで Qiita を徘徊していたところ、Scalaで作ったWebアプリをDockerizeして動かすという素晴らしい記事を発見できました。

とはいえ、上記は 3 年前の記事ということで若干手直しが必要な部分もありましたので、改めて記事としてまとめておくことにしました。参考になれば幸いです。

(2022-09-06追記) さらに 1 年が経過したので諸々のバージョンアップに対応しました。sbt-native-packager の配布元が com.typesafe.sbt から com.github.sbt へ移動したことに注意が必要です。

(2022-09-11追記) Akka-HTTP を含む Akka ファミリのライセンス変更に伴い、Scalatra 版の記述を追加しました。

(2024-09-04追記) Akka 版を OSS フォークである Pekko に差し替えました。また、Scalatra 版を Scalatra 3.1(Jakarta Servlet 6.1 / Jetty 12) に対応させました。

環境

Scala + Pekko-HTTP で Web アプリを作成する

最初に、build.sbt に依存ライブラリを追加します。元記事との違いとしては、 Akka ファミリを Pekko ファミリに置き換えたほか、SLF4J 対応のロギングライブラリとして logback を追加しています。1

build.sbt
ThisBuild / scalaVersion := "3.3.3"

// sbt run でサーバを起動したまま維持できるようにします
run / fork := true

libraryDependencies ++= Seq(
  "org.apache.pekko" %% "pekko-actor-typed" % "1.1.0",
  "org.apache.pekko" %% "pekko-stream" % "1.1.0",
  "org.apache.pekko" %% "pekko-http" % "1.0.1",
  "ch.qos.logback" % "logback-classic" % "1.4.0",
)

続いてソース本文です。Akka HTTP 10.2.0 での仕様変更を反映しています。

main.scala
import org.apache.pekko.actor.typed.ActorSystem
import org.apache.pekko.actor.typed.scaladsl.Behaviors
import org.apache.pekko.event.Logging
import org.apache.pekko.http.scaladsl.Http
import org.apache.pekko.http.scaladsl.model._
import org.apache.pekko.http.scaladsl.server.Directives._
import scala.concurrent.Await
import scala.concurrent.duration.Duration
import org.slf4j.{Logger, LoggerFactory}

object Main {
  val logger = LoggerFactory.getLogger(getClass)

  def main(args: Array[String]): Unit = {

    // typed ActorSystem が導入されましたが、旧 ActorSystem も利用可能です。
    implicit val system: ActorSystem[Any] = ActorSystem(Behaviors.empty, "my-sample-app")

    // GET /indexでリクエストのURLパラメータとUserAgentを返却する
    val 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.getOrElse("http.host", "0.0.0.0")
    val port = sys.props.getOrElse("http.port", "8080").toIntOption match {
      case Some(port) => port
      case None => {
        logger.error("システムプロパティ http.port には整数値を指定してください")
        8080
      }
    }

    // akka.http.scaladsl.HttpExt.bindAndHandle が非推奨になりました
    val f = Http().newServerAt(host, port).bind(route)

    println(s"server at [$host:$port]")

    Await.ready(f, Duration.Inf)
  }
}

sbt run でコンパイル・実行し、 http://localhost:8080/index?<クエリ>=<値> にアクセスして以下のように表示されたら成功です。

$ curl -f "http://localhost:8080/index?query=string"
param: Map(query -> string), user-agent: curl/7.66.0}

Scala + Scalatra で Web アプリを作成する

基本的に公式ドキュメントに従いましょう。Generate a Scalatra project にテンプレートからプロジェクトを作成する方法が記載されていますが、Jakarta Servlet 6.0 / Jetty 12 対応が追い付いていないようです。

Scalatra はマイクロな Web フレームワークであり、Scalatra で作成した Web アプリは Java サーブレットになります。上記のコードのうち、ルーティングに相当する部分をサーブレットとして切り出します:

MyScalatraServlet.scala
import org.scalatra._
import org.slf4j.{Logger, LoggerFactory}

class MyScalatraServlet extends ScalatraServlet {
  val logger = LoggerFactory.getLogger(getClass)

  // GET /indexでリクエストのURLパラメータとUserAgentを返却する
  get("/index") {
    contentType = "text/plain"

    val str =
      s"param: ${params.toMap}, user-agent: ${request.getHeader("User-Agent")}"
    logger.info(str)

    Ok(str)
  }

}

サーブレットは実行に Jetty や Tomcat などのサーブレット・コンテナを必要とします。これはシンプルな HTTP ツールキットである Akka-HTTP / Pekko-HTTP とは異なる点ですね。sbt-assembly を利用して FAT JAR にしたり、sbt-native-packager を利用して Dockerize するためには、Jetty を起動するメインクラスを用意します:

Main.scala
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.ee10.servlet.{DefaultServlet, ServletContextHandler}
import org.eclipse.jetty.ee10.webapp.WebAppContext
import org.scalatra.servlet.ScalatraListener
import org.slf4j.{Logger, LoggerFactory}

object Main {
  val logger = LoggerFactory.getLogger(getClass)

  def main(args: Array[String]): Unit = {
    start().join()
  }

  def start(): Server = {
    val context = new WebAppContext()
    context.setContextPath("/")
    context.setBaseResourceAsString(
      this.getClass.getResource("Main.class").toURI.resolve(".").toString
    )
    context.addEventListener(new ScalatraListener)
    context.addServlet(classOf[DefaultServlet], "/")

    val port = sys.props.getOrElse("http.port", "8080").toIntOption match {
      case Some(port) => port
      case None => {
        logger.error("システムプロパティ http.port には整数値を指定してください")
        8080
      }
    }

    val server = new Server(port)
    server.setHandler(context)
    server.start()

    server
  }
}

特にそれらへのこだわりがなければ sbt package コマンドで WAR ファイルを生成し、適当なサーブレット・コンテナの Docker イメージにデプロイしてもいいでしょう。

sbt-native-packager で Docker イメージを作成する

project/plugins.sbtsbt-native-packager を追加します。同プラグインは msi | rpm | deb などのネイティブパッケージのほか、Docker イメージも出力できるすぐれものです。

project/plugins.sbt
addSbtPlugin("com.github.sbt" % "sbt-native-packager" % "1.10.4")

build.sbt に Docker ビルド用の設定を追加します。

build.sbt
// DockerPlugin は JavaAppPackaging に依存します
enablePlugins(JavaAppPackaging)
enablePlugins(DockerPlugin)

// 普段 Dockerfile で指定する内容を記載します
// MAINTAINER タグは非推奨になったので記載の必要はありません。
Docker / packageName := "sample-webapp" // イメージ名に反映されます
Docker / version := "2.0.0" // タグに反映されます
dockerBaseImage := "eclipse-temurin:latest" // 利用したい JDK/JRE イメージが指定できます
dockerExposedPorts := List(8080)

その他 DockerPlugin で利用可能な設定は公式マニュアルを参照してください。

sbt Docker/publishLocal で Docker イメージがビルドできます:

$ sbt Docker/publishLocal
(中略)
[success] All package validations passed
[info] Sending build context to Docker daemon  26.37MB
[info] Step 1/20 : FROM eclipse-temurin:latest as stage0
(中略)
[info] Built image sample-webapp with tags [2.0.0]
[success] Total time: 7 s, completed 2021/03/06 23:17:24

Dockerfile の生成のみを行うこともできます:

sbt Docker/stage

生成された Dockerfile は以下のようになっていました:

target/docker/stage/Dockerfile
FROM eclipse-temurin:latest as stage0
LABEL snp-multi-stage="intermediate"
LABEL snp-multi-stage-id="7943caea-0791-42a0-8d29-55430bc54cae"
WORKDIR /opt/docker
COPY 2/opt /2/opt
COPY 4/opt /4/opt
USER root
RUN ["chmod", "-R", "u=rX,g=rX", "/2/opt/docker"]
RUN ["chmod", "-R", "u=rX,g=rX", "/4/opt/docker"]
RUN ["chmod", "u+x,g+x", "/4/opt/docker/bin/scalatra-example"]

FROM eclipse-temurin:latest as mainstage
USER root
RUN id -u demiourgos728 1>/dev/null 2>&1 || (( getent group 0 1>/dev/null 2>&1 || ( type groupadd 1>/dev/null 2>&1 && groupadd -g 0 root || addgroup -g 0 -S root )) && ( type useradd 1>/dev/null 2>&1 && useradd --system --create-home --uid 1001 --gid 0 demiourgos728 || adduser -S -u 1001 -G root demiourgos728 ))
WORKDIR /opt/docker
COPY --from=stage0 --chown=demiourgos728:root /2/opt/docker /opt/docker
COPY --from=stage0 --chown=demiourgos728:root /4/opt/docker /opt/docker
EXPOSE 8080
USER 1001:0
ENTRYPOINT ["/opt/docker/bin/scalatra-example"]
CMD []

マルチステージビルドを活用していていい感じですね。

ソース全文は GitHub 上のリポジトリ(Pekko / Scalatra)に置いておきましたので、ご参考まで。

参考リンク

  1. logback は設定ファイルを追加しない場合ロギング出力は直接コンソールに出力されるので、コンテナレディなアプリを作成するには最適です。

2
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
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?