AWSLambdaをGradleからデプロイできるようにしてGroovyで楽に書こう
2015年6月からJavaでLambdaが書けるようになって久しいですが、活用してますか?SIerお得意のJavaです。夢のサーバーレス、試さないわけにはいきません。Groovyを使って解説します。
ソフト道場の「SIerが目利きする。今日から使えるAWSレシピ」 第1回
- 2016年08月03日公開
はじめに
NTTソフトウェアの須藤です。
2015年6月からJavaでLambdaが書けるようになって久しいですが、みなさん活用してますか?Java8の Lambda式 の方じゃなくて、 AWS Lambda の方のLambdaですよ。あぁややこしい。JavaですよJava。SIerお得意のJavaです。夢のサーバーレス、試さないわけにはいきません。
まことに残念ながら、個人の健康上の理由でJavaを気軽に書くことができないので、以降Groovyを使って解説します。
build.gradle
まず、プロジェクトをGradleでビルド・デプロイするために必要なファイルを用意します。
AWSの公式ドキュメントAWS Lambda » .zip デプロイパッケージの作成 (Java)でもGradleでのビルド方法と build.gradle
のサンプルが紹介されていますが、作成したzip(jar)を手動でマネジメントコンソールからアップロードする形です。これは、何度も繰り返しデプロイしたりJenkinsなどのCIツールを活用したい場合、いささか不便です。
なので、ここではクラスメソッド株式会社様の都元ダイスケ氏が中心となって開発し、Apache License 2.0で公開されている gradle-aws-plugin を使います。
用意した build.gradle
はこんな形です。
import com.amazonaws.services.lambda.model.* import jp.classmethod.aws.gradle.lambda.*
buildscript { repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath "jp.classmethod.aws:gradle-aws-plugin:0.+" } }
apply plugin: 'java' apply plugin: 'groovy' apply plugin: 'jp.classmethod.aws' apply plugin: 'jp.classmethod.aws.lambda'
repositories { mavenCentral() }
dependencies { compile( 'com.amazonaws:aws-lambda-java-core:1.+', 'org.codehaus.groovy:groovy-all:2.+' ) }
jar { into('lib') { from configurations.compile } }
aws { profileName "default" region "ap-northeast-1" proxyHost = "proxy.example.co.jp" // modify for your environment proxyPort = 8080 // modify for your environment }
def myFunctionName = "aws-lambda-groovy" // modify for your function name
task migrateFunction(type: AWSLambdaMigrateFunctionTask, dependsOn: build) { functionName = myFunctionName role = "arn:aws:iam::${aws.accountId}:role/lambda-poweruser" // need this in your IAM role runtime = Runtime.Java8 zipFile = jar.archivePath handler = "example.App::handler" // application handler FQCN::method memorySize = 256 // default 128 MB timeout = 30 // default 3 sec }
task invokeFunction(type: AWSLambdaInvokeTask) { functionName = myFunctionName invocationType = InvocationType.RequestResponse payload = file("testdata.json") doLast { println "Lambda function status : " + invokeResult.statusCode println "Lambda function result : " + new String(invokeResult.payload.array(), "UTF-8") println "InvokeResult debug info: " + invokeResult.toString() } }
task deleteFunction(type: AWSLambdaDeleteFunctionTask) { functionName = myFunctionName }
buildscript
Gradleタスク実行時にgradle-aws-pluginをダウンロードし使えるようにするために必要な設定です。
apply plugin
GradleのGroovyプラグインを使ってGroovyを含むプロジェクトのビルドができるようにしています。また、gradle-aws-pluginの共通機能とLambda向け機能を build.gradle
内で使えるようにしています。
dependencies
AWSが用意している aws-lambda-java-core
ライブラリとGroovyの全機能 groovy-all
をクラスパスに置き、これから作成するLambda関数ハンドラ内で利用できるようにしています。今回 aws-lambda-java-events
は利用しないので記述していませんが、他のAWSマネージドサービスからイベントを受け取るLambda関数ハンドラを作る場合には必要になります。
jar
依存ライブラリをjarファイル内の lib
ディレクトリに配置し、デプロイパッケージに含めるための記述です。
aws
gradle-aws-pluginが使うcredentialとリージョン、プロキシの設定をしています。
~/.aws/credentials
ファイルを作成し、マネジメントコンソールでIAMユーザーを作成したときに取得した aws_access_key_id
と aws_secret_access_key
を記述しておきましょう。 profileName "default"
は、このファイル内の [default]
プロファイルを指します。当然、別のプロファイル名で別のアクセスキーを記述しておくこともできます。
[default] aws_access_key_id = ABCDEFGHIJKLMNOPQRST aws_secret_access_key = 40chars-SECRET-ACCESS-KEY-you-got-in-csv
[dev] aws_access_key_id = A******************* aws_secret_access_key = another-SECRET-ACCESS-KEY-you-got-in-csv
プロキシの無い環境では、 proxyHost
と proxyPort
の記述は不要です。
migrateFunction
タスク
作成したLambda関数ハンドラのデプロイ(アップロード)をするためのタスクで、そのために必要な設定を記述しています。
functionName
には任意の名称を付けてください。同じでないと困るので、 myFunctionName
を使って複数のタスクで共通化しています。ひとつの build.gradle
内で複数のLambda関数を管理する場合、タスク名と functionName
をそれぞれ意識して管理しましょう。
role
ではIAMロールを指定しています。 lambda-poweruser
ロールを使う設定ですので、マネジメントコンソールから lambda-poweruser
という名前のロールを作成し、 AWSLambdaFullAccess
ポリシーをアタッチしておきましょう。今回は使いませんが、LambdaからVPC内にアクセスさせたい場合 AWSLambdaVPCAccessExecutionRole
ポリシーも必要になりますので、アタッチしておくと便利です。
runtime
には Runtime.Nodejs
、Runtime.Nodejs43
、Runtime.Java8
、Runtime.Python27
のいずれかが指定できます。今回はGroovyを使うので Java8
です。
zipFile
は、アップロードするパッケージファイルを指定します。通常、ビルドしたjarファイルをアップロードするのでこのままで良いでしょう。 s3File
でS3上のファイルを指定することもできます。
handler
は実行するLambda関数ハンドラのFQCNとメソッド名です。今回は example.App
クラス内に handler()
メソッドを作るので、 example.App::handler
となります。
memorySize
はLambda上で確保するメモリサイズです。デフォルト値は128MBですが、必要に応じて設定しましょう。この記事の内容であれば128MBでも実行できます。
timeout
はLambda関数ハンドラの最大実行時間です。300秒までの任意の時間を設定できます。デフォルト3秒なので、ある程度時間のかかる処理をさせたい場合必ず変更しましょう。
invokeFunction
タスク
デプロイしたLambda関数ハンドラを実行するためのタスクで、そのために必要な設定を記述しています。
invocationType
に指定可能なのは InvocationType.Event
、InvocationType.RequestResponse
、InvocationType.DryRun
のいずれかです。今回は、リクエストを投げてレスポンスを貰う形式で利用するので RequestResponse
です。
payload
は、Lambda関数ハンドラを呼ぶリクエストパラメータのJSONを指定します。 payload = '{"key":"value"}'
のように直接JSONを記述することもできますが、 payload = file("testdata.json")
のようにファイルで指定し、そのファイルを編集した方が良いでしょう。
doLast
は、レスポンスを受け取った後に実行する処理です。処理結果を画面に表示します。
deleteFunction
タスク
デプロイしたLambda関数ハンドラを削除するタスクです。
src/main/groovy/example/App.groovy
Lambda関数ハンドラ本体です。GradleのGroovyプラグインのデフォルトのソースコード配置先が src/main/groovy/
なので、ここに作成します。
Hello world!
package example
import com.amazonaws.services.lambda.runtime.Context import com.amazonaws.services.lambda.runtime.LambdaLogger
public class App { Closure say = { "Hello world!" }
public String handler(Map json, Context context) { LambdaLogger logger = context.logger logger.log("json = " + json.toString()) return say() } }
Groovyの機能が使える確認として Hello world!
の文字列を返すクロージャ say
を定義しています。
Lambda関数ハンドラの入出力の型として String
、Integer
、Boolean
、Map
、List
、POJO、ストリームがサポートされていますが、ここではGroovyでの操作に便利な Map
を入力に利用することにします。
Lambda上でログを出力するには、 LambdaLogger
を使います。 context.getLogger()
でインスタンスを取得できます。Groovyでは context.logger
でgetterが呼び出せます。 logger.log()
で出力したログは、CloudWatch Logsから確認できます。 CloudWatch > ロググループ > /aws/lambda/{functionName}
のログストリームを開いておくと良いでしょう。
return
している値がLambda関数ハンドラのレスポンスになります。
testdata.json
の中には、仮で {"key":"value"}
と書いておきます。
さぁ実行してみましょう!ここではGradle Wrapperの gradlew
コマンドを使います。最初は migrateFunction
タスクです。初回のみ、依存ライブラリのダウンロードが行われるため時間がかかります。
> ./gradlew migrateFunction :compileJava :compileGroovy :processResources :classes :jar :assemble :compileTestJava :compileTestGroovy :processTestResources :testClasses :test :check :build :migrateFunction
BUILD SUCCESSFUL
Total time: 14.565 secs
これでLambda関数ハンドラがAWS上にデプロイされました。マネジメントコンソールから確認すると、指定した名前で追加されているはずです。
Lambda関数ハンドラの実行は invokeFunction
タスクです。
> ./gradlew invokeFunction :invokeFunction Lambda function status : 200 Lambda function result : "Hello world!" InvokeResult debug info: {StatusCode: 200,Payload: java.nio.HeapByteBuffer[pos=0 lim=14 cap=14]}
BUILD SUCCESSFUL
Total time: 10.604 secs
Hello world!
できましたね!
CloudWatch Logsを見ると、STARTとENDの間にこんな行があるはずです。
json = [key:value]
GroovyでJSONをイジる
testdata.json
が、日本のオープンデータ都市一覧のRDF/JSONのような少し複雑なJSONでも、特定の値を抜き出す処理はさほど難しくありません。
package example
import com.amazonaws.services.lambda.runtime.Context import com.amazonaws.services.lambda.runtime.LambdaLogger
public class App { public Map handler(Map json, Context context) { LambdaLogger logger = context.logger logger.log("json = " + json.toString())
def yokohama = [] json.each { entry -> if (entry.value.'http://www.w3.org/2000/01/rdf-schema#label'[0].'value'.contains("横浜")) { yokohama.add(entry.key) } }
Map result = [ timestamp: new Date().format("yyyy-MM-dd HH:mm:ss Z", TimeZone.getTimeZone("Asia/Tokyo")), urls : yokohama ] return result } }
ここでは、 http://www.w3.org/2000/01/rdf-schema#label
内の配列の最初の要素の value
に 横浜
を含む自治体のオープンデータ公開URLの一覧を返します。 Groovy-JDK の Date#format() を使って、日時もあわせて Map
に格納しています。
Lambda関数ハンドラの出力として Map
型を利用するとJSONに変換してくれるため、invokeFunction
の結果はこんな形になります。(もちろん invokeFunction
前に migrateFunction
を忘れずに!)
> ./gradlew invokeFunction :invokeFunction Lambda function status : 200 Lambda function result : {"timestamp":"2016-06-06 11:52:54 +0900","urls":["http://www.city.yokohama.lg.jp/nishi/opendata/","http://www.city.yokohama.lg.jp/kanazawa/kz-opendata/","http://www.city.yokohama.lg.jp/seisaku/seisaku/opendata/"]} InvokeResult debug info: {StatusCode: 200,Payload: java.nio.HeapByteBuffer[pos=0 lim=213 cap=213]}
BUILD SUCCESSFUL
Total time: 14.604 secs
おわりに
LambdaでJavaを使う場合、起動レイテンシが問題になることがあります。1回目・2回目のLambda関数ハンドラの実行は、メモリ使用量とクラスロードの容量に応じて時間がかかり、3回目以降の実行が10~20分程度のウインドウ時間内であればコンテナが再利用されメモリ使用量に比例して高速に実行され、ウインドウ時間を過ぎると1回目・2回目のパフォーマンスに戻る、という挙動になります。
前記のLambda関数ハンドラを5回連続実行したときの実行時間のサンプルです。
1回目:Duration: 4810.34 ms Billed Duration: 4900 ms Memory Size: 256 MB Max Memory Used: 111 MB 2回目:Duration: 1059.48 ms Billed Duration: 1100 ms Memory Size: 256 MB Max Memory Used: 111 MB 3回目:Duration: 576.81 ms Billed Duration: 600 ms Memory Size: 256 MB Max Memory Used: 111 MB 4回目:Duration: 223.22 ms Billed Duration: 300 ms Memory Size: 256 MB Max Memory Used: 111 MB 5回目:Duration: 358.42 ms Billed Duration: 400 ms Memory Size: 256 MB Max Memory Used: 111 MB
このように、実行時間に大きな差が出るので、それが困る、というユースケースの場合にはCloudWatch Eventsなどを利用して定期的にLambda関数ハンドラを実行する仕組みを仕込んでおくと良いでしょう。
Java系でもLambda、こわくないでしょう簡単ですよ!