AWS Lambda で Play(Scala) のバッチ処理を実行する
ぶらいじぇん
はじめに
この記事は、サーバサイドエンジニアの平凡な日常で得た AWS Lambda と Play(Scala) の基本的な使い方を淡々と説明するものです。
過度な期待はしないでください。
それはとっても嬉しいなって
みさなまどうも。サーバサイドのエンジニアぶらいじぇんです。
AWS の Lambda で Java が使えるようになりましたね!
AWS Lambda Update – Run Java Code in Response to Events
今回はこの AWS Lambda 上で Play(Scala) アプリケーションのバッチ処理を実行してみます。
さっそくやってみよう
公式サイトにあった、ような・・・・・
Scala で書いたコードを AWS Lambda で実行する方法は、Writing AWS Lambda Functions in Scala で説明されています。今回はそれを参考にしました。
上記サイトで紹介されているの方法(Lambda☆Scala)のポイントは
- sbt-assembly plugin を使っていい感じの jar ファイルを生成する
- AWS Lambda の Upload a .Zip file に生成した jar ファイルをアップロードする
この手順であれば、 Play でも同じことができそうです。
わたしの、サンプルの実装
上記のポイントを Play アプリケーションに取り込みます。
以下がそのコードです。
プラグインとライブラリの設定
project/plugins.sbt に以下を追記します。
build.sbt に以下を追記します
// aws-lambda
libraryDependencies ++= Seq(
"com.amazonaws" % "aws-lambda-java-core" % "1.0.0",
"com.amazonaws" % "aws-lambda-java-events" % "1.0.0"
)
// assembly
assemblyMergeStrategy in assembly := {
case PathList("javax", "servlet", xs @ _*) => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".html" => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".properties" => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".xml" => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".types" => MergeStrategy.first
case PathList(ps @ _*) if ps.last endsWith ".class" => MergeStrategy.first
case "application.conf" => MergeStrategy.concat
case "unwanted.txt" => MergeStrategy.discard
case x => (assemblyMergeStrategy in assembly).value(x)
}
ここでの assemblyMergeStrategy in assembly ...
の記述は Play のプロジェクトで依存するライブラリが同じクラスパスのファイルなどを持っている場合を想定して書いています。
バッチ処理の実装
app/example/PlayTask.scala を以下のように新規に作成します。
package example
import com.amazonaws.services.lambda.runtime.events.S3Event
import play.api._
class PlayTask {
// AWS Lambda Handler
def exec(event: S3Event): String = withApplication { app =>
// app.configuration.getString("play.lambda.greet").getOrElse("")
Play.current.configuration.getString("play.lambda.greet").getOrElse("")
}
private def withApplication[A](f: Application => A): A = {
val env = Environment(new java.io.File("."), getClass.getClassLoader, Mode.Prod)
val context = ApplicationLoader.createContext(env)
val app = ApplicationLoader(context).load(context)
try {
Play.start(app)
f(app)
} finally {
Play.stop(app)
}
}
}
def exec(event: S3Event): String
が AWS Lambda のイベントハンドラです。
Play.start(app)
(20行目) を実行すると Play アプリケーションが起動状態になり、 Play.stop(app)
(23行目) を実行すると Play アプリケーションが停止状態になります。つまり、withApplication
に渡す関数が Play アプリケーションを実行している状態で実行されます(21行目)。
conf/application.conf に以下を追記します。
assembly って、ほんと便利
sbt assembly を実行します。
以下のようなログが表示されます。環境にもよりますが、少し時間がかかります。
[info] Loading project definition from /Users/katou/git/play-lambda/project
[info] Set current project to play-lambda (in build file:/Users/katou/git/play-lambda/)
[info] Compiling 1 Scala source to /Users/katou/git/play-lambda/target/scala-2.11/classes...
[info] Including from cache: jta-1.1.jar
[info] Including from cache: jackson-core-2.5.3.jar
...
(長いので省略しています)
...
[info] Packaging /Users/katou/git/play-lambda/target/scala-2.11/play-lambda-assembly-1.0-SNAPSHOT.jar ...
[info] Done packaging.
[success] Total time: 29 s, completed 2015/06/27 16:02:21
ビルドに成功すると ./target/scala-2.11
に jar ファイルが生成されます。デフォルトの設定ではアプリケーション名やそのバージョンによってファイル名が変わります。今回は play-lambda-assembly-1.0-SNAPSHOT.jar
です。
以上で AWS Lambda にアップロードする jar ファイルを作ることができました。
これ以降は AWS の Web Console で作業をします。
AWS Lambda で実行する
Java8 の選択肢があるんだよ
[Create a Lambda function]から、新しくLambdaに登録する画面を開きます。
以下のように入力して[Create Lambda function]します(ボタンをクリック)。
※ Upload a .Zip file と表示されていますが、拡張子は jar のままで問題ありません。
※ jar のファイルサイズが50MBを超える場合は Upload a .Zip from S3 を選択しましょう。
※ 上の画像で Role の lambda_s3_exec_role
は S3 execution role から新規に作成したものです。
テスト実行する
[Action] -> [Edit/Test]から、編集・テスト実行する画面を開きます。
今回は S3Event をハンドリングするコードを書いていますので、以下のように[S3 Put]を選択します。
[Invoke]すると、問題がなければ以下の画像ような実行結果が表示されます。
"Hello, AWS Lambda in Play"
の文字列が表示されます。
Option なんて、あるわけない
余談です。
試しに実装していたときに Handler となる関数(ここではexec
)の戻り値の型を Option[String]
にしていました。
// AWS Lambda Handler
def exec(event: S3Event): Option[String] = withApplication { app =>
// app.configuration.getString("play.lambda.greet")
Play.current.configuration.getString("play.lambda.greet")
}
そうすると、以下の画像のように Execution result
の表示が微妙になります。
考えてみれば Java に Option なんてないわけですし、String(JSON文字列) を返すとよさそうです。
最後に残った Event 登録
さいごに Event の登録が必要です。忘れずにやっておきましょう。
動作確認が終わったら[Action] -> [Add event source]から、実際にハンドリングする Event を登録する画面を開きます。
以下のように入力して[Submit]すれば完了です。
さいごに
この記事で書いた app/example/PlayTask.scala
の withApplication
は、実はもっとよい方法がある気がしてなりません。1)Play アプリケーション内で Akka を使ってスケジュールリングする方法は、バッチ処理のために常駐させることはしたくないため使いません。
Play のバッチ処理のベストプラクティスを見つけることができていません。2)それがわかれば。。。もう何も怖くない!
こういうやり方がありますよって情報を待ってます!!