情報畑でつかまえてロゴ
本サイトは NTTテクノロスが旬の IT をキーワードに
IT 部門が今知っておきたい最新テクノロジーに関する情報をお届けするサイトです

ソフト道場の「SIerが目利きする。今日から使えるAWSレシピ」 第1回

AWSLambdaをGradleからデプロイできるようにしてGroovyで楽に書こう

2015年6月からJavaでLambdaが書けるようになって久しいですが、活用してますか?SIerお得意のJavaです。夢のサーバーレス、試さないわけにはいきません。Groovyを使って解説します。

はじめに

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_idaws_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

プロキシの無い環境では、 proxyHostproxyPort の記述は不要です。

migrateFunction タスク

作成したLambda関数ハンドラのデプロイ(アップロード)をするためのタスクで、そのために必要な設定を記述しています。

functionName には任意の名称を付けてください。同じでないと困るので、 myFunctionName を使って複数のタスクで共通化しています。ひとつの build.gradle 内で複数のLambda関数を管理する場合、タスク名と functionName をそれぞれ意識して管理しましょう。

role ではIAMロールを指定しています。 lambda-poweruser ロールを使う設定ですので、マネジメントコンソールから lambda-poweruser という名前のロールを作成し、 AWSLambdaFullAccess ポリシーをアタッチしておきましょう。今回は使いませんが、LambdaからVPC内にアクセスさせたい場合 AWSLambdaVPCAccessExecutionRole ポリシーも必要になりますので、アタッチしておくと便利です。

runtime には Runtime.NodejsRuntime.Nodejs43Runtime.Java8Runtime.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.EventInvocationType.RequestResponseInvocationType.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関数ハンドラの入出力の型として StringIntegerBooleanMapList、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 Wrappergradlew コマンドを使います。最初は 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-JDKDate#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、こわくないでしょう簡単ですよ!

著者プロフィール
AWSチーム
AWSチーム