AWS Lambda で Play(Scala) のバッチ処理を実行する

ぶらいじぇん
44

はじめに

この記事は、サーバサイドエンジニアの平凡な日常で得た 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 に以下を追記します。

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.13.0")

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 に以下を追記します。

play.lambda.greet = "Hello, AWS Lambda in Play"

assembly って、ほんと便利

sbt 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]します(ボタンをクリック)。

AWS_Lambda_1

Upload a .Zip file と表示されていますが、拡張子は jar のままで問題ありません。
※ jar のファイルサイズが50MBを超える場合は Upload a .Zip from S3 を選択しましょう。
※ 上の画像で Role の lambda_s3_exec_roleS3 execution role から新規に作成したものです。


テスト実行する

[Action] -> [Edit/Test]から、編集・テスト実行する画面を開きます。
今回は S3Event をハンドリングするコードを書いていますので、以下のように[S3 Put]を選択します。

AWS_Lambda_2

 


[Invoke]すると、問題がなければ以下の画像ような実行結果が表示されます。

AWS_Lambda_3

"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 の表示が微妙になります。

AWS_Lambda_4

考えてみれば Java に Option なんてないわけですし、String(JSON文字列) を返すとよさそうです。

最後に残った Event 登録

さいごに Event の登録が必要です。忘れずにやっておきましょう。
動作確認が終わったら[Action] -> [Add event source]から、実際にハンドリングする Event を登録する画面を開きます。
以下のように入力して[Submit]すれば完了です。

AWS_Lambda_5

さいごに

この記事で書いた app/example/PlayTask.scalawithApplication は、実はもっとよい方法がある気がしてなりません。1)Play アプリケーション内で Akka を使ってスケジュールリングする方法は、バッチ処理のために常駐させることはしたくないため使いません。
Play のバッチ処理のベストプラクティスを見つけることができていません。2)それがわかれば。。。もう何も怖くない!

こういうやり方がありますよって情報を待ってます!!

脚注   [ + ]

1. Play アプリケーション内で Akka を使ってスケジュールリングする方法は、バッチ処理のために常駐させることはしたくないため使いません。
2. それがわかれば。。。もう何も怖くない!