そういえばScalaをAWS Lambdaで実行するのどうするんだっけ、と思った。実はScalaをLambdaで動かしたことは無い気がするので調べておいた。今回はJava 17を前提に動作させる。
今回書いたサンプルプロジェクトはこちら。
tl;dr
- sbt-assemblyを使ってUber JARを生成する
- AWS LambdaをJavaランタイムでセットアップする
- エントリポイントを指定する
- JARをアップロードする
- 動く
AWS LambdaのJavaランタイム
けっこう前から、AWS LambdaはJavaランタイムをサポートするようになっている。
JavaをコンパイルしたJARファイルをアップロードすると、それを実行してくれるようになっている。
ScalaもJVM言語なのでJARファイルを生成をできるし、Javaランタイムからすれば違いは分からないので、JARをアップロードしてエントリポイントを指示すると普通に実行できる。
sbt-assembly
Scala、JARときたらsbt-assemblyを使うのがほぼ定番の選択肢だ。sbt-assemblyは、sbtプロジェクトのScalaソフトウェアを1つの実行可能なJARファイル(UberJARという)にビルドしてくれるプラグインだ。
sbt-assemblyを使ってプロジェクトをビルドするには、以下の2つを行えば最低限の設定ができる:
- sbt-assemblyプラグインを利用することを宣言する
- 1つのJARファイル内でファイル名の衝突が起こった場合どうするか指定する
それぞれ見ていこう。
sbt-assemblyへの依存性の宣言
project/plugins.sbt
がなければ作成し、以下のように依存性を指定する。sbt-assemblyは、執筆時点での最新版を利用している。
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.1")
ファイル名衝突時の設定
ふつうScalaやJavaは、クラスごとにクラスファイルというものが生成されるが、JARファイルはこれを一手にパッケージングするフォーマットだ。するとファイル名が衝突してしまうことがたまにある(logbackの設定などがよく衝突する)。JARを生成するためにはこうした衝突が発生したときにどうするかを設定しなければならない。
build.sbt
に以下のように設定しよう:
ThisBuild / assemblyMergeStrategy := { case PathList("META-INF", xs @ _*) => MergeStrategy.discard case x => MergeStrategy.first }
META-INF
ファイルが衝突したら両方とも削除し、それ以外の場合は先にあったファイルを優先する。
その他のsbtの設定
これで最低限Uber JARを生成する準備はできたが、Lambda固有の設定もしておこう。
これは公式ブログに書いてあったことなので今も必要なのかは不明だが、実行時のランタイムとなるJavaのバージョンをコンパイルオプションで指定しておく。
build.sbt
の先頭らへんに以下のように記述する:
javacOptions ++= Seq("-source", "17", "-target", "17", "-Xlint")
最終的なbuild.sbt
最終的にbuild.sbt
は以下のようになる:
val scala3Version = "3.2.0" javacOptions ++= Seq("-source", "17", "-target", "17", "-Xlint") lazy val root = project .in(file(".")) .settings( name := "aws-lambda-scala-exercise", version := "0.1.0-SNAPSHOT", scalaVersion := scala3Version, retrieveManaged := true, libraryDependencies += "com.amazonaws" % "aws-lambda-java-core" % "1.2.2", libraryDependencies += "com.amazonaws" % "aws-lambda-java-events" % "3.11.2" ) ThisBuild / assemblyMergeStrategy := { case PathList("META-INF", xs @ _*) => MergeStrategy.discard case x => MergeStrategy.first }
aws-lambda-java-core
とaws-lambda-java-events
は、Lambdaで各種のイベントを受け取るときの型を定義しているライブラリだ。今回は使わなかったが、参考文献を参照して活用してほしい。
コード本体
さて、今回は単純にHello Worldを行うだけのプログラムを実行してみよう。src/main/scala/com/github/windymelt/lambdaexercise/Main.scala
*1 に以下のコードを書いた:
package com.github.windymelt.lambdaexercise object Main { @main def hello: Unit = println("Hello world!") println(msg) def msg = "I was compiled by Scala 3. :)" }
ビルド
コードを書いたら、sbt assembly
を実行してJARファイルを生成する。target/scala-3.2.0/aws-lambda-scala-exercise-assembly-0.1.0-SNAPSHOT.jar
が生成されるはずだ。
AWS Lambda側の設定
まずはLambda関数を作成しよう。
アーキテクチャはお好みでよいが、ランタイムは現時点で最新のJava 17に設定する。
次に設定を開いてエントリポイントとなるメソッドを指定する。今回はcom.ginthub.windymelt.lambdaexercise.Main::hello
がエントリポイントだ。
次に生成されたJARファイルをアップロードする。今回はそれほどデカくないが、デカくなりすぎた場合はS3を検討しよう。
これで準備は完了だ。テストタブから適当なダミーデータを使ってテストを実行すると、数秒の後で成功するはずだ:
初回はクラスロード等に少し時間がかかるため600msかかってしまった。ランタイムが活きている間は、次回以降の実行は非常に高速だ:
1msちょっとで動作した。JITによる最適化もかかるだろうから、速度の面ではあまり心配はなさそう。数十分経過するとランタイムは停止してまた元の状態に戻る。
まとめと感想
- Scala 3のUber JARを使ってAWS LambdaでScala 3の簡単なサンプルを実行した。
- ライブラリを使ってイベントを処理できることを紹介した。
- 初回実行は少し時間がかかるが二回目からは高速になることを紹介した。
JARをあげればそのまま動くというのがかなり良くて、例えばコンテナを挟まなければならなかったりすると起動時間がより不利になっていたかもしれない。JARのカジュアルさは大きいと思う。JVMをそのまま使えるから、GraalVMやScala Nativeの変な地雷を踏み抜くということがなく、普通に動いてくれる。JVMだから普通に手元で動くようなコードが動作することを想定してよくて、例えばcatsやhttp4sなども使える。今後SmithyによってAWSの機能を呼び出すといったことも可能かもしれない。
とはいえScalaのUberJARはふくらみがちなので、実用上はS3を経由してアップロードすることになるだろうと思う。Dockerイメージよりはマシだと思う。
参考文献
Javaを使ったLambdaのサンプルが掲載されている。要するにHandlerの型を合わせて、Lambdaのコンソールからそのメソッドをエントリポイントとして指定すればよい。
追記
特にScalaに限ったノウハウではありませんが、デプロイ方式をコンテナイメージにすることで便利になる部分があるかもしれませんhttps://t.co/WeiYvV1UcU
— べりんぐ (@_Bassari) 2023年5月22日
Lambdaを動かすにはコンテナランタイムというのもある。こちらはECSを使ってカスタムランタイムを動かすというちょっと大掛りな手法だが、Lambdaのアーティファクトサイズが250MiBであるという制限(Scalaはけっこう当たりそう)を回避できるという利点がある。そのかわりカスタムランタイムを書くことになるのでちょっと面倒。
実行環境に関しては特に拘りなければ標準提供のJavaとして実行でいいと思いますが、GraalVMのバイナリでカスタムランタイム化する作戦もあるようです(Scala on Lambdaを自分で動かしたことがないので伝聞ですけれども)https://t.co/liByGNxxo1https://t.co/N6T6WazWsk
— べりんぐ (@_Bassari) 2023年5月22日
初動を爆速にしなければならず、しかも呼ばれる間隔が広くてランタイムが止まってしまう、といった場合にはGraalVM Native Imageの利用も検討するとよさそう。
*1:sbt new scala/scala3.g8のテンプレートで出てくるやつとほぼ同じ内容