AWS Lambda Functionをシングルコマンドでデプロイする
よく訓練されたアップル信者、都元です。AWS Lambdaはみなさんもう既にご利用でしょうか? 2014年のre:Inventで発表され、先日はJava8にも対応しました。CloudFormationによるAWS Lambda Functionリソースの定義にも対応し、東京リージョンにも登場。着々と都元のLambda対応が進んでいることを実感する日々です。あとは、CloudFormationでJava8ランタイムのFunctionを定義できるようになれば…。
AWS Lambdaシングルコマンドデプロイ
さて、都元と言えばシングルコマンドデプロイに命を燃やしていることで有名(?)です。下記を主張し続けてもう10年近くになります(遠い目)
リポジトリからコードをチェックアウトし、 必要に応じて環境固有の設定をした後、
コマンド1つで起動・デプロイができるべきである。Miyamoto, Daisuke (2006)
そりゃもう、AWS Lambdaだって例外ではありません。当然シングルコマンドでデプロイできるべきです。まぁ結論から言えば、まだ少々道半ばではありますが、現状での成果をアウトプットすべく、本エントリーを書いています。
みなさんも動かしてみることができるように、対象となるサンプルプロジェクトはGitHubに上げておきました。
実装したFunction
Javaで書いてみました。そして外部のライブラリ(google guava)に敢えて依存し、依存ライブラリがあるFunctionもきちんとデプロイできることを検証しました。
package jp.classmethod.aws.sample.lambda; import java.nio.charset.Charset; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.google.common.io.BaseEncoding; public class Base64Encoder implements RequestHandler<String, String> { private static final Charset UTF_8 = Charset.forName("UTF-8"); @Override public String handleRequest(String input, Context context) { LambdaLogger logger = context.getLogger(); logger.log("input = " + input); String output = BaseEncoding.base64().encode(input.getBytes(UTF_8)); logger.log("output = " + output); return output; } }
というのがFunction本体の全文です。単純に、入力値となる文字列をBase64エンコーディングして返す、というシンプルな仕様です。
AWS Lambda Functionバンドルの作成
今回、プロジェクトのビルド及びデプロイにはGradleというツールを利用します。プロジェクト直下のbuild.gradle
というファイルが、Gradleの設定ファイルです。先のプロジェクトではGradle Wrapperという仕組みを仕込んでいますので、試してみる環境にGradleがインストールされている必要はありません。
さて、外部の依存ライブラリを含むAWS Lambda Functionは、通常のjarファイル内の直下lib
ディレクトリ内に依存ライブラリを突っ込む、という構造のjarファイルを作成します。通常の単純なJava環境では、このようなjarを作っても依存したクラスを見つけられずにエラーとなりますが、AWS Lambda環境下では、上手くクラスがロードできるように工夫がされているものと考えられます。
というわけで、このような構造のjarファイルを作成するためのGradleの設定は、下記のような感じです。
jar { into('lib') { from configurations.compile } }
これをファイルの中のどこかに書いておけばOKです。
$ git clone https://github.com/classmethod-aws/aws-lambda-single-command-deploy-example.git $ cd aws-lambda-single-command-deploy-example $ ./gradlew jar $ unzip -l build/libs/aws-lambda-single-command-deploy-example-0.1-SNAPSHOT.jar Archive: build/libs/aws-lambda-single-command-deploy-example-0.1-SNAPSHOT.jar Length Date Time Name -------- ---- ---- ---- 0 07-07-15 14:15 META-INF/ 25 07-07-15 14:15 META-INF/MANIFEST.MF 0 07-07-15 14:15 jp/ 0 07-07-15 14:15 jp/classmethod/ 0 07-07-15 14:15 jp/classmethod/aws/ 0 07-07-15 14:15 jp/classmethod/aws/sample/ 0 07-07-15 14:15 jp/classmethod/aws/sample/lambda/ 1982 07-07-15 14:15 jp/classmethod/aws/sample/lambda/Base64Encoder.class 0 07-07-15 14:15 lib/ 5538 07-07-15 11:38 lib/aws-lambda-java-core-1.0.0.jar 2256213 03-16-15 23:44 lib/guava-18.0.jar -------- ------- 2263758 11 files
という感じで、望み通りの形式になっていることが分かるでしょうか。
Functionバンドルをデプロイする(解説編)
さて、このjarファイルをAWSにデプロイしなければなりません。
今回のキモとなるのはgradle-aws-pluginです。裏でコソコソとAWSリソースを操作するGradleプラグインを作っているのですが、未だに仕様が固まっていないので今ひとつpublicに発表しづらい状態です。華やかなデビューはもうしばらくお待ちくださいw ドキュメントではなくexampleによる説明が大部分ですが、もしご興味がありましたらREADMEをざっとご覧頂ければ。
さて話を戻しまして。
aws { profileName "default" region "ap-northeast-1" } task migrateFunction(type: AWSLambdaMigrateFunctionTask, dependsOn: build) { functionName = project.functionName role = "arn:aws:iam::${aws.accountId}:role/lambda-poweruser" runtime = Runtime.Java8 zipFile = jar.archivePath handler = "jp.classmethod.aws.sample.lambda.Base64Encoder" }
build.gradle
内に上記のような宣言をしておくと、./gradlew migrateFunction
というコマンド1つでAWS Lambda Functionをデプロイ可能です。
この記述はそこそこ細かく説明しておきましょう。
profileName
という設定は、~/.aws/credentials
内に宣言したプロファイル名です。詳しくは【鍵管理】~/.aws/credentials を唯一のAPIキー管理場所とすべし【大指針】等を御覧ください。region
はデプロイ先のリージョンですね。難しいことはないと思います。migrateFunction
タスクの定義で、AWSLambdaMigrateFunctionTask
型を指定しています。また、dependsOn buildとすることによって、jarファイルを生成した後にこのタスクが走るように制御しています。functionName
は任意のFunction名です。build.gradle
の23行目で宣言した変数を参照しているだけです。role
はFunction実行時に与えられるAWS APIを操作するためのロールです。今回の例ではFunction内でAWS APIをコールしないので利用しませんが、CreateFunction操作にあたってRoleの指定が必須であるために指定しています。lambda-poweruser
という名前のロールは予め作成しておいてください。(というのがシングルコマンドデプロイに反します。 TODO いつか倒す。)runtime
はJava8を選択しました。NodeJSにおけるシングルコマンドデプロイをしたい場合はこのあたりをご参考に。zipFile
は、jarファイルのローカル環境下でのパス指定です。jarタスクが位置を知っていますので、そのarchivePath
プロパティから位置を取り出しています。handler
はFunctionのエントリポイントとなるクラスの指定です。
Functionバンドルをデプロイする(実践編)
さてやってみましょう。まず、検証環境にはFunctionが一つも無いことを確認。
$ aws lambda list-functions { "Functions": [] }
そしてコマンド1つでデプロイします。
$ ./gradlew migrateFunction :clean :compileJava :processResources UP-TO-DATE :classes :jar :assemble :compileTestJava UP-TO-DATE :processTestResources :testClasses :test UP-TO-DATE :check UP-TO-DATE :build :migrateFunction Function not found: arn:aws:lambda:ap-northeast-1:000011112222:function:base64encoder:HEAD (Service: AWSLambda; Status Code: 404; Error Code: ResourceNotFoundException; Request ID: 617ce8cb-xxxx-xxxx-xxxx-4b61e6ab18b0) BUILD SUCCESSFUL Total time: 3.128 secs
Function not found
というメッセージが出ていますが、初回作成なので既存のFunctionが無かったことを伝えています。既に同名のFunctionがある場合は、実装を更新します。さて、Functionはデプロイされたでしょうか。
$ aws lambda list-functions { "Functions": [ { "FunctionName": "base64encoder", "MemorySize": 128, "CodeSize": 2000474, "FunctionArn": "arn:aws:lambda:ap-northeast-1:000011112222:function:base64encoder", "Handler": "jp.classmethod.aws.sample.lambda.Base64Encoder", "Role": "arn:aws:iam::000011112222:role/lambda-poweruser", "Timeout": 3, "LastModified": "2015-07-07T05:31:13.908+0000", "Runtime": "java8", "Description": "" } ] }
問題ありませんね。では呼び出してみましょう。このコマンドは、src/test/resources/input.txt
を入力値として送信し、出力値(結果)をbuild/out.txt
に書き出す、という指定をしています。
$ cat src/test/resources/input.txt "よく訓練されたアップル信者、都元です。" $ aws lambda invoke --function-name base64encoder --payload $(cat src/test/resources/input.txt) build/out.txt { "StatusCode": 200 }
200レスポンスということで、成功したようですね。内容もBase64っぽい。
$ cat build/out.txt "44KI44GP6KiT57e044GV44KM44Gf44Ki44OD44OX44Or5L+h6ICF44CB6YO95YWD44Gn44GZ44CC"
jq
を使ってダブルクオートを外し、base64
コマンドでデコードしてみます。
$ cat build/out.txt | jq -r '.' | base64 -d よく訓練されたアップル信者、都元です。
成功しましたね! ちなみにFunctionの呼び出しは、Gradleからも可能です。
$ ./gradlew invokeFunction :invokeFunction Lambda function result: "44KI44GP6KiT57e044GV44KM44Gf44Ki44OD44OX44Or5L+h6ICF44CB6YO95YWD44Gn44GZ44CC" BUILD SUCCESSFUL Total time: 3.335 secs
最後に、作ったFunctionを消しておきましょう。
$ ./gradlew deleteFunction :deleteFunction BUILD SUCCESSFUL Total time: 1.778 secs
まとめ
Java8ランタイムのAWS Lambda Functionを、Gradleを使ってシングルコマンド(一歩手前)デプロイしてみました。これで、Functionの実装を書き換えては、すぐにdeployというサイクルを高速に回せますねー。