Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Google CloudのCloud FunctionsでScalaを実行する

仕事でCloud Functionsを触る機会があったのでメモ。この記事はScala Advent Calendar 2023の2日目の記事です。

qiita.com

昨日の記事はこちら。

zenn.dev

AWSがLambdaを提供しているのと同じく、Google CloudはCloud Functionsというサービスを提供している。Cloud Functionsは様々なランタイムをサポートしているが、Java 17もサポートしているため、Scalaも動作させられる。

cloud.google.com

この記事では、Cloud FunctionsでペライチのScalaファイルを動作させる方法を説明する。

Functionの概要

このFunctionは、JSON形式でHTTPリクエストを受け取り、nameフィールドを使って"Hello, $name!"を返す。

ファイルの準備

今回もScala CLIを使ってScalaをビルドする。Scala CLIについては以下記事を参照。

blog.3qe.us

まず全体のファイルを以下に示す:

//> 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を構成して実行できるのでこういう用途にはかなり便利だと思う。

参考文献

cloud.google.com

tarao.orezdnu.org

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?