仕事でCloud Functionsを触る機会があったのでメモ。この記事はScala Advent Calendar 2023の2日目の記事です。
昨日の記事はこちら。
AWSがLambdaを提供しているのと同じく、Google CloudはCloud Functionsというサービスを提供している。Cloud Functionsは様々なランタイムをサポートしているが、Java 17もサポートしているため、Scalaも動作させられる。
この記事では、Cloud FunctionsでペライチのScalaファイルを動作させる方法を説明する。
Functionの概要
このFunctionは、JSON形式でHTTPリクエストを受け取り、name
フィールドを使って"Hello, $name!"
を返す。
ファイルの準備
今回もScala CLIを使ってScalaをビルドする。Scala CLIについては以下記事を参照。
まず全体のファイルを以下に示す:
//> using scala "3.3.0" //> using dep "com.google.cloud:libraries-bom:26.22.0" //> using dep "com.google.cloud.functions:functions-framework-api:1.0.1" //> using dep "io.circe::circe-core:0.14.1" //> using dep "io.circe::circe-parser:0.14.1" package functions import com.google.cloud.functions.{HttpFunction, HttpRequest, HttpResponse} import io.circe._, io.circe.parser._, io.circe.syntax._ class ScalaHelloWorld extends HttpFunction { override def service( httpRequest: HttpRequest, httpResponse: HttpResponse ): Unit = { val lines = LazyList .continually(httpRequest.getReader.readLine()) .takeWhile(_ != null) val name = parse(lines.mkString("\n")).right.get.hcursor .downField("name") .as[String] .right .get httpResponse.getWriter.write(s"Hello, $name!") } }
Cloud Functionsを動作させるには、com.google.cloud
にあるいくつかのライブラリが必要になる。circe
はJSON用のライブラリなので、好きなやつを使えばよい。
おおまかには、HttpFunction
を継承したクラスを用意すれば良い。service
メソッドをoverrideして、渡ってくるHttpRequest
からbodyを取り出して使えばよい。
bodyはBufferedReader
として渡ってくるので、ScalaらしくLazyList
にして扱いやすくしてから操作している。
JSONのパースにはここではCirceを使っているが、使い慣れているライブラリを使えば良いと思う。
最後にhttpResponse
に結果を書き込んで終了だ。
余談: recoord4s使ってJSONまわりの型定義を生やす
record4sを使うと、ダミーデータを元にJSONデコード用の型定義を生やせて便利だ:
//> using scala "3.3.0" //> using dep "com.google.cloud:libraries-bom:26.22.0" //> using dep "com.google.cloud.functions:functions-framework-api:1.0.1" //> using dep "io.circe::circe-core:0.14.1" //> using dep "io.circe::circe-parser:0.14.1" //> using dep "io.circe::circe-generic:0.14.1" //> using dep "com.github.tarao::record4s:0.9.0" //> using dep "com.github.tarao::record4s-circe:0.9.0" package functions import com.google.cloud.functions.{HttpFunction, HttpRequest, HttpResponse} import io.circe._, io.circe.parser._, io.circe.syntax._, io.circe.generic.auto._ import com.github.tarao.record4s.% import com.github.tarao.record4s.circe.Codec.decoder class ScalaHelloWorld extends HttpFunction { override def service( httpRequest: HttpRequest, httpResponse: HttpResponse ): Unit = { val lines = LazyList .continually(httpRequest.getReader.readLine()) .takeWhile(_ != null) val example = %(name = "foobar") val Right(data) = decodeByExample(lines.mkString("\n"), example) httpResponse.getWriter.write(s"Hello, ${data.name}!") } def decodeByExample[R <: %](json: String, example: R)(using io.circe.Decoder[R] ): Either[?, R] = for { jsonObj <- parse(json) record <- jsonObj.as[R] } yield record }
JAR入りZIPの生成
Cloud FunctionsはJavaランタイムに対応しており、そのファイル形式としてビルド済みのFat JARにも対応している。このため、手元でJARファイルにしてからアップロードすればそのままScalaのコードを動かせる。
ただし、Lambdaと違ってCloud Functionsは直接JARをアップロードできず、いったんZIPに入れる必要がある(JAR自体もZIPの一種なので、無駄だと思うが・・・)
まずScalaファイルをビルドしてJARファイルを生成する:
% scala-cli package --assembly -o jartest.jar --preamble=false jartest.scala
--assembly
はFat JARを生成させるオプション--preamble=false
は、JARを直接実行しないからエントリポイントまわりの処理はいらないよと伝えるオプション- これが無いと
./jartest.jar
みたいに実行してもうまく起動するためのコード(preamble)がJARファイルに埋め込まれる - しかし今回はエントリポイントがないのでpreambleの用意のしようがない
- これが無いと
次にZIPファイルを作る:
% zip jartest.jar.zip jartest.jar
Cloud Functionsの作成
Java 17ランタイムで、基本的に指示通りにする。
エントリポイントはパッケージ.クラス名
の形式で指定する。
Cloud Functionsではテストデータをfunctionに送り付けることができるので、{"name": "Windymelt"}
を送りつけてみる。
すると無事実行結果が得られた。
Hello, Windymelt!
まとめ
Cloud FunctionsでScalaコードを実行する最小限のコードを紹介した。Fat JARを用意すればよいのでsbtプロジェクトにも応用できるし、Cloud Buildを使って自動デプロイを構成することもできるはずだ。Scala CLIは単一ファイルでScalaを構成して実行できるのでこういう用途にはかなり便利だと思う。