🥰

Http4s で書いた Scala のサーバーを cloud run にデプロイする

2022/09/13に公開

Http4s で書いた Scala のサーバーを cloud run にデプロイする最小構成の例

サーバーを sbt-native-packager プラグインで Docker イメージにしてリリースします.

http4s は Scala の Typelevel エコシステムのウェブサーバーを書くためのライブラリです.
Typelevel エコシステムのライブラリははやくから Scala 3 対応が進んでいます. もちろん http4s も Scala 3 で書くことができます.

https://http4s.org/

build.sbt にプロジェクトを定義してライブラリを追加します.

json ハンドリングをしないので core,dsl,ember-server モジュールだけでOk です.

build.sbt
val V = new {
  val scala213 = "2.13.8"
  val http4s = "1.0.0-M36"
  val CE = "3.3.12"
}
lazy val server = project.in(file("server"))
  .enablePlugins(JavaAppPackaging)
  .enablePlugins(DockerPlugin)
  .settings(
    run / fork := true,
    scalaVersion := V.scala213,
    libraryDependencies ++= Seq(
      "org.http4s" %% "http4s-core" % V.http4s,
      "org.http4s" %% "http4s-dsl" % V.http4s,
      "org.http4s" %% "http4s-ember-server" % V.http4s,
      "org.typelevel" %% "cats-effect" % V.CE
    )
  )
  .settings(
    Docker / packageName := "<region>.gcr.io/<project-name>/path/to/image",
    // Docker / packageName に gcr のレポジトリを含め代わりに
    // Docker /packageName := imageName
    // dockerRepository := <region>.gcr.io/<project-name>
    // としてもいい.
    dockerExposedPorts ++= Seq(8080,8080),
    dockerUpdateLatest := true, // latest tag を更新する場合はこれを true に設定する. デフォルトは false.
    dockerBuildCommand := {
        if (sys.props("os.arch") != "amd64") {
         dockerExecCommand.value ++ Seq("buildx", "build", "--platform=linux/amd64", "--load") ++ dockerBuildOptions.value :+ "."
       } else dockerBuildCommand.value
     }
   )

以下の部分は、arm の端末で amd64 向けにイメージをビルドするためのワークアラウンド.
arm 向けにビルドすると cloud run でサーバーを動かすことができないので docker buildx コマンドを使ってクロスビルドする必要があります.

    if (sys.props("os.arch") != "amd64") {
         dockerExecCommand.value ++ Seq("buildx", "build", "--platform=linux/amd64", "--load") ++ dockerBuildOptions.value :+ "."
       } else dockerBuildCommand.value
     }

Docker イメージをビルドするために sbt-native-packager プラグインを追加します.

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

サーバーを以下のように実装します.

server/src/main/scala/Server.scala
// Blaze は古いので Ember を使う
import org.http4s.ember.server.EmberServerBuilder
// IO の runtime の管理や main 関数を省略するために IOApp を使う
// (Scala の App trait の cats effect 版のようなもの)
import cats.effect.IOApp
import org.http4s.dsl._
import com.comcast.ip4s._
// dsl.io._ をインポートすると、akka のようなルーティング DSL が使える.
import org.http4s.dsl.io._
import cats.effect.{ExitCode, IO}
import org.http4s.HttpRoutes

object Main extends IOApp {
    val pingPong = HttpRoutes.of[IO] {
        case GET -> Root / "ping" =>
            Ok("pong")
    }
    def run(args: List[String]): IO[ExitCode] =
        EmberServerBuilder
          .default[IO]
          .withHost(ipv4"0.0.0.0")
          .withPort(port"8080")
          .withHttpApp(pingPong.orNotFound)
          .build
          .useForever
          .as(ExitCode.Success)
}

イメージをビルドします. 以下のコマンドで "<region>.gcr.io/<project-name>/path/to/image:0.1.0-SNAPSHOT という名前の Docker イメージがローカルに生成されます. この名前は build.sbt の Docker / packageName で変えられます.

sbt docker:publishLocal

以下のコマンドで docker push <region>.gcr.io/... で GCP の Container Registry にイメージを push できるようにします.

gcloud auth login
gcloud auth configure-docker

gcr にアプリケーションのイメージを push します.

docker push <region>.gcr.io/<project-name>/path/to/image:0.1.0-SNAPSHOT

あとは GCP のコンソールにログインして cloud run の画面を開き、push したイメージを選択してリリースすればサーバーを公開できます.

http4s はさまざまなライブラリと組み合わせることができます. 例えば circe を使って json を受け取って返すサーバーを作ったり、tapir を使ってルーティング定義から openapi を自動生成したりすることができます. シンプルな API を提供しているので Play Framework を使うほどでもないちょっとした API サーバーを立てるケースにも活躍します.

Discussion