LambrollというOSSがある。
AWS Lambdaのデプロイを簡単にしてくれるツールで、非常に使い勝手が良いので仕事で愛用している。
そんなLambrollと、同様に簡単にScalaを書けるようにしてくれるScala CLIとを組み合わせてみたらどうなるだろう、と思って実験してみたところ、すごく簡単にAWS Lambda関数を定義できることがわかったので紹介したい。
今回利用したソースコードはGitHubに公開している:
Scala CLI
Scala CLIはScala用のスクリプティングツールで、Node.jsでいうところのDenoみたいな存在。
Scala.js
今回はScalaをJVMにコンパイルするのではなく、JavaScriptをターゲットにトランスパイルして、Node.jsランタイムでLambdaを実行してみようと思う。JSにトランスパイルすることで、高速な起動時間とV8エンジンのパフォーマンスを得ることができる。
ScalaをJSにトランスパイルするには、Scala.jsを利用すればよい:
Scala CLIでは、--jsオプションをつけるだけでScala.jsが起動して勝手にJSにトランスパイルして実行したりパッケージを作ったりしてくれる。今回はこれを使って楽をするという作戦だ。
下準備
あらかじめAWSのコンソールでNode.jsランタイムのLambda関数を作成しておき、これをlambroll initで取り込んでおく:
λ lambroll --profile=windymelt --region=ap-northeast-1 init --function-name=scalambroll-exercise
関数を書く
index.scalaを作成して関数定義を書こう。JSのランタイムとのインターフェイスで少し気遣いが必要だが、それ以外は普通のScalaだ:
//> using platform js //> using scala 3.7 import scala.scalajs.js import scala.scalajs.js.annotation.* // 各種パラメータなどの型定義 @js.native // こんな感じの型が渡ってくるけど中身はJS側が用意します、というアノテーション trait Event extends js.Object { val name: String = js.native // js.nativeが指定されているところは、「値はJSランタイムで用意するのでコンパイラは気にしないでください」、という意味 } @js.native trait Context extends js.Object // こちらは我々が返す型なのでjs.nativeはない trait Response extends js.Object { val statusCode: Int val body: String } // 本体 object Index { @JSExportTopLevel(name = "handler", moduleID = "index") // JSファイルへの出力を制御するアノテーション。ここではindexモジュールのhandlerという関数としてexportしてください、と明示している def handler( event: Event, context: Context, callback: js.Function2[Null, Response, Unit] // JS側の関数を呼び出したいときは若干型の配慮が必要になっている ): Unit = { println(s"Hello, ${event.name}!") val resp = new Response { val statusCode: Int = 200 val body: String = s"Hello, ${event.name}!" } callback(null, resp) } } // ダミーオブジェクト(Scala CLIの不具合でいったんこうしている) // https://github.com/VirtusLab/scala-cli/issues/3964 object Hello { def main(args: Array[String]): Unit = 42 }
callback styleはdeprecatedになっているようなので、以下のようにしてpromise styleで書いてもよい:
/* 略 */ object Index { @JSExportTopLevel(name = "handler", moduleID = "index") def handler( event: Event, context: Context ): js.Promise[Response] = { println(s"Hello, ${event.name}!") val resp = new Response { val statusCode: Int = 200 val body: String = s"Hello, ${event.name}!" } js.Promise.resolve(resp) } } /* 略 */
タスクランナー
今回はMakefileにビルド用のタスクを書いておく:
.PHONY: all build deploy clean
build: dist/index.js
all: build
clean:
rm -rf dist
dist/index.js: index.scala
scala-cli package -f --js --js-module-kind es -o dist index.scala
dist/package.json: # ランタイムにESMであるということを明示するためにpackage.jsonを用意する
mkdir -p dist
echo '{ "main": "index.js", "type": "module" }' > dist/package.json
deploy: dist/index.js dist/package.json
lambroll --profile=windymelt deploy --src=dist
scala-cli package -f --js --js-module-kind es -o dist index.scala としている部分がScala CLIを利用してJSファイルを出力している箇所だ。Scala.jsを利用して、ESMとして、distディレクトリに、index.scalaをコンパイルした結果を出力してください、という意味。
あとはそのまま出力先ディレクトリをlambrollに渡せばデプロイできる。
デプロイ
make deployすれば終わり。
λ make deploy scala-cli package -f --js --js-module-kind es -o dist index.scala Compiling project (Scala 3.7.4, Scala.js 1.20.1) [warn] ./index.scala:39:41 [warn] Discarded non-Unit value of type Int. Add `: Unit` to discard silently. [warn] def main(args: Array[String]): Unit = 42 [warn] ^^ Compiled project (Scala 3.7.4, Scala.js 1.20.1) Wrote /home/windymelt/src/github.com/windymelt/scalambroll-exercise/dist/main.js, run it with node ./dist/main.js lambroll --profile=windymelt deploy --src=dist 2025/11/20 22:35:52 [info] lambroll v1.3.2 2025/11/20 22:35:52 [info] loading Function from function.json 2025/11/20 22:35:52 [info] starting deploy function scalambroll-exercise 2025/11/20 22:35:52 [info] creating zip archive from dist 2025/11/20 22:35:52 [info] zip archive wrote 14291 bytes 2025/11/20 22:35:52 [info] updating function configuration 2025/11/20 22:35:52 [info] updating function configuration ... 2025/11/20 22:35:52 [info] State:Active LastUpdateStatus:Successful 2025/11/20 22:35:52 [info] updating function configuration accepted. waiting for LastUpdateStatus to be successful. 2025/11/20 22:35:52 [info] State:Active LastUpdateStatus:InProgress 2025/11/20 22:35:52 [info] waiting for LastUpdateStatus Successful 2025/11/20 22:35:53 [info] State:Active LastUpdateStatus:InProgress 2025/11/20 22:35:53 [info] waiting for LastUpdateStatus Successful 2025/11/20 22:35:56 [info] State:Active LastUpdateStatus:Successful 2025/11/20 22:35:56 [info] updating function configuration successfully 2025/11/20 22:35:56 [info] updating function code ... 2025/11/20 22:35:56 [info] State:Active LastUpdateStatus:Successful 2025/11/20 22:35:56 [info] updating function code accepted. waiting for LastUpdateStatus to be successful. 2025/11/20 22:35:56 [info] State:Active LastUpdateStatus:InProgress 2025/11/20 22:35:56 [info] waiting for LastUpdateStatus Successful 2025/11/20 22:35:57 [info] State:Active LastUpdateStatus:InProgress 2025/11/20 22:35:57 [info] waiting for LastUpdateStatus Successful 2025/11/20 22:35:59 [info] State:Active LastUpdateStatus:InProgress 2025/11/20 22:35:59 [info] waiting for LastUpdateStatus Successful 2025/11/20 22:36:04 [info] State:Active LastUpdateStatus:Successful 2025/11/20 22:36:04 [info] updating function code successfully 2025/11/20 22:36:04 [info] deployed version 9 2025/11/20 22:36:04 [info] updating alias set current to version 9 2025/11/20 22:36:04 [info] alias updated
試しに{"name": "windymelt"}を渡して実行してみよう。

やった〜。
まとめ
- Scala CLIを利用すると簡単にScalaをトランスパイルしたESMを作成できることを確認した
- Lambrollを利用してScalaコードをそのままAWS Lambdaとしてデプロイできることを確認した
Scala CLIとLambrollとは簡単で実用的な組み合わせだ。型安全なソフトウェアを手軽に利用できるし、Scala CLIにはパッケージマネージャが同梱されているため、設定ファイルが一切必要ないのが非常にうれしいポイントだ。