初めてのAWS Lambda（どんな環境で動いているのかみてみた）

月曜日から３日間、AWSアーキテクト研修でした。そこではじめてLambdaに接しまして、ひととおり驚いてきたところです。自分なりのまとめです（ご存知の方には釈迦に説法）。

# Lambdaとは？
恥ずかしながら私は「サーバーレス」という言葉を聞いてもいまいちピンと来ていませんでした。ですが、これは文字通りなんですよね。プログラムが動作する環境なんてどうだっていいんです。OSがなんだとか、ミドルウェアがなんだとか、メモリがどれくらいでCPUがいくらで、NWがどうで、トポロジーがなんでとか、そんなことはどうでもいいんです（実際はメモリサイズを指定できて、それに応じたCPU能力が割当たりますが）。
とにかく、「トリガー」と呼ばれる"きっかけ"を契機に、コード（プログラム）が動くんです。`Java`ならJVMがおもむろに立ち上がって、アップロードしておいた`jar`が実行されるんです。「トリガー」はAWSサービスと高度に統合されていて、例えば

+ ファイルストレージサービスであるところのS3（もはや単なるストレージの域を超越していますが）にファイルがアップロードされた
+ メッセージがキューにputされた
+ API Gatewayにリクエストがきた
+ EC2インスタンスが起動した
+ ３時になった
+ おなかがすいた（とAmazon echoに話した）
+ e.t.c.

MDBならぬTDB（Trigger Driven Bean）でしょうか。Beanである必要もないので、TDC（Trigger Driven Code）とでも言ったほうがいいのかもしれません。

# うごかしてみる
研修の間、実習時間に余裕があったので、研修端末に入っていたEclipseで簡単なコードを書いて動かしてみました。テストはAWS Consoleからキックできるので、特に「トリガー」を定義しなくても動かすだけなら簡単に試せます。

## お作法
基本的にどんな`Java`プログラムでも必要なライブラリを組み込んでおけば動きますが、コールするメソッドにはお約束があるようです。それは引数です。第一引数に`Object`をもらい、第二引数に`Context`をもらいます。メソッド名はなんでもいいです。型もなんでもいいです（ただし第一引数と戻り値の型ともに`Seriarizable`である必要あり。プリミティブ型もOK）。

第一引数に入るのは、具体的には「トリガー」からの情報です。メッセージがキューに`put`されたことをトリガーとするのであれば、そのメッセージ自体を渡してあげたり。戻り値は同期呼び出しであればほぼそのまんまでしょう。インタフェース要件に従って、`Serialize`して返してあげればよいだけです。

第二引数の`Context`ですが、これは`javax.naming.Context`ではなく、`com.amazonaws.services.lambda.runtime.Context`です。というわけで、[AWSが提供するjarファイル](https://mvnrepository.com/artifact/com.amazonaws/aws-lambda-java-core/1.1.0)をビルドパスに追加する必要があります。[^1]
[^1]: Eclipseには[AWSのツールキットプラグイン](https://docs.aws.amazon.com/ja_jp/toolkit-for-eclipse/v1/user-guide/setup-install.html)があるので、この環境をセットアップしておくだけでも可です。

## 作る
まだ意味のあるコードを書くほどの技量もアイディアもないので、インフラ屋っぽくどんな環境（システムプロパティ、環境変数、渡された`Context`オブジェクト）で動いているのかみてみることにしました。

```java:SystemInfo.class
package net.mognet.aws.lambda;

import java.util.Map;
import java.util.Properties;

import com.amazonaws.services.lambda.runtime.Context;

public class SystemInfo {

    public static String printSystemInfo(int i, Context context) {
    		StringBuilder sb = new StringBuilder();
    		//ヘッダを追加
    		sb.append("name,value\n");
    		
    		//システムプロパティ取得
    		Properties prop = System.getProperties();
    		for(Object key : prop.keySet()) {
    			String name = (String) key;
    			String value  = prop.getProperty(name);
    			sb.append(name + "," + value + "\n");
    		}
    		//環境変数取得
    		Map<String, String> env = System.getenv();
    		for(String key : env.keySet()) {
    			String value = env.get(key);
    			sb.append(key + "," + value + "\n");
    		}
    		//Contextの情報を取得
    		sb.append("context" + "," + context.toString());
    		return sb.toString();
    }
    
    public static void main(String[] args) {
    		System.out.println(printSystemInfo(1, null));
    }
}
```

`main`はテスト用です。１個目の引数こそ本来は大事なんでしょうけど今回は何もしません。

## 乗せる
AWSコンソールを開いてLambdaの関数を作ります（関数という単位で動きます。複数の関数をオーケストレーションするサービスもあるようです（詳細未調査））。
![スクリーンショット 2018-01-31 21.39.16.png](https://qiita-image-store.s3.amazonaws.com/0/232587/71877be0-ab57-8c09-6663-5eea85360cf7.png)
適当に名前とランタイム（今回はJava8）を選んで「関数の作成」を押します。
標準出力はCloudWatchLogsへ流れるので、事前にCloudWatchLogsへのWrite権限のあるロールを作って必要に応じてここでアタッチしてください。
![スクリーンショット 2018-01-31 21.39.50.png](https://qiita-image-store.s3.amazonaws.com/0/232587/c4e7c2f6-50ff-0f0d-7590-41e0d6cad203.png)
![スクリーンショット 2018-01-31 21.39.57.png](https://qiita-image-store.s3.amazonaws.com/0/232587/7b03685e-144b-6ff2-9b4d-b4676af2c879.png)
本来ならここでトリガーを選んで云々となりますが、とにかくテストしてみたいだけなので、その辺の条件だけいれます。
![スクリーンショット 2018-01-31 21.40.05.png](https://qiita-image-store.s3.amazonaws.com/0/232587/ac55096f-eb8e-e2e1-1d8c-34eccf76b45c.png)
関数コードのところで、「アップロード」から`jar`ファイルをアップロード、大事なのが「ハンドラ」でここに実行するメソッドを入力します。書き方が決まっていて、"."表記でクラスのフルパスの後ろに"`::`"をつけてメソッド名です。
今回は"`net.mognet.aws.lambda.SystemInfo::printSystemInfo`"となります。ついでに環境変数もつけてみました。一旦「保存」すると実際にファイルがアップロードされます。
![スクリーンショット 2018-01-31 21.41.03.png](https://qiita-image-store.s3.amazonaws.com/0/232587/a39887a5-69c5-0bb2-4605-bdaacdf1ff38.png)
次にテストの準備です。テストケース（入力設定＝第一引数設定）です。画面上部の「テストイベントの設定」を選びます。
![スクリーンショット 2018-01-31 21.41.19.png](https://qiita-image-store.s3.amazonaws.com/0/232587/d7cd066d-9d81-c099-9cf6-892240760c33.png)
実行するメソッド`public static String printSystemInfo`の第一引数が`int`なので、1とだけ書いて終わりです。下の方にある「保存」を押します。これでテスト準備完了です。

## いざ実行！
おもむろに「テスト」を押します。
![スクリーンショット 2018-01-31 21.42.15.png](https://qiita-image-store.s3.amazonaws.com/0/232587/23bbadc0-c246-8912-80d7-54dea61a607b.png)
動きました。今回はログ出力（標準出力）なしなので、ログは見ませんが開始と終了のメッセージが出ていました。`String`型のメソッドを実行したので、`return`した文字列がそのまま画面上に表示されています（改行コードは改行してほしかったけど実行結果表示コンソールとしてはこれが正しいあり方ですね）。

## 付録
付録で実行結果を載せておきます。

|name|value|
|:--|:--|
|java.runtime.name|OpenJDK Runtime Environment|
|sun.boot.library.path|/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/amd64|
|java.vm.version|25.141-b16|
|java.vm.vendor|Oracle Corporation|
|java.vendor.url|http://java.oracle.com/|
|path.separator|:|
|java.vm.name|OpenJDK 64-Bit Server VM|
|file.encoding.pkg|sun.io|
|user.country|US|
|sun.java.launcher|SUN_STANDARD|
|sun.os.patch.level|unknown|
|java.vm.specification.name|Java Virtual Machine Specification|
|user.dir|/|
|java.runtime.version|1.8.0_141-b16|
|java.awt.graphicsenv|sun.awt.X11GraphicsEnvironment|
|java.endorsed.dirs|/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/endorsed|
|os.arch|amd64|
|java.io.tmpdir|/tmp|
|line.separator||
|java.vm.specification.vendor|Oracle Corporation|
|os.name|Linux|
|sun.jnu.encoding|UTF-8|
|java.library.path|/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib|
|java.specification.name|Java Platform API Specification|
|java.class.version|52.0|
|sun.management.compiler|HotSpot 64-Bit Tiered Compilers|
|os.version|4.9.77-31.58.amzn1.x86_64|
|user.home|/home/sbx_user1066|
|user.timezone|UTC|
|java.awt.printerjob|sun.print.PSPrinterJob|
|file.encoding|UTF-8|
|java.specification.version|1.8|
|java.class.path|/var/runtime/lib/LambdaJavaRTEntry-1.0.jar|
|user.name|sbx_user1066|
|java.vm.specification.version|1.8|
|sun.java.command|/var/runtime/lib/LambdaJavaRTEntry-1.0.jar|
|java.home|/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre|
|sun.arch.data.model|64|
|user.language|en|
|java.specification.vendor|Oracle Corporation|
|awt.toolkit|sun.awt.X11.XToolkit|
|java.vm.info|mixed mode, sharing|
|java.version|1.8.0_141|
|java.ext.dirs|/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/ext:/usr/java/packages/lib/ext|
|sun.boot.class.path|/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/resources.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/rt.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/sunrsasign.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jsse.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jce.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/charsets.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/lib/jfr.jar:/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.141-1.b16.32.amzn1.x86_64/jre/classes|
|java.vendor|Oracle Corporation|
|file.separator|/|
|java.vendor.url.bug|http://bugreport.sun.com/bugreport/|
|sun.io.unicode.encoding|UnicodeLittle|
|sun.cpu.endian|little|
|sun.cpu.isalist||
|PATH|/usr/local/bin:/usr/bin/:/bin|
|_AWS_XRAY_DAEMON_ADDRESS|169.254.79.2|
|LAMBDA_TASK_ROOT|/var/task|
|AWS_LAMBDA_FUNCTION_MEMORY_SIZE|128|
|TZ|:UTC|
|AWS_SECRET_ACCESS_KEY|`secret`|
|AWS_EXECUTION_ENV|AWS_Lambda_java8|
|AWS_DEFAULT_REGION|ap-northeast-1|
|AWS_LAMBDA_LOG_GROUP_NAME|/aws/lambda/SystemInfo|
|XFILESEARCHPATH|/usr/dt/app-defaults/%L/Dt|
|_HANDLER|net.mognet.aws.lambda.SystemInfo::printSystemInfo|
|LANG|en_US.UTF-8|
|LAMBDA_RUNTIME_DIR|/var/runtime|
|AWS_SESSION_TOKEN|`tokenString`|
|AWS_ACCESS_KEY_ID|`accessKeyId`|
|LD_LIBRARY_PATH|/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib|
|_X_AMZN_TRACE_ID|Root=1-5a71b98c-393aaa7b51f5612a348586c0;Parent=3ff8164301e3ccd4;Sampled=0|
|AWS_SECRET_KEY|`secretKey`|
|hogehoge|gehogeho|
|AWS_REGION|ap-northeast-1|
|AWS_LAMBDA_LOG_STREAM_NAME|2018/01/31/[$LATEST]29640ec0ac8e426ab2b0a041b3a1b1f4|
|AWS_XRAY_DAEMON_ADDRESS|169.254.79.2:2000|
|_AWS_XRAY_DAEMON_PORT|2000|
|NLSPATH|/usr/dt/lib/nls/msg/%L/%N.cat|
|AWS_XRAY_CONTEXT_MISSING|LOG_ERROR|
|AWS_LAMBDA_FUNCTION_VERSION|$LATEST|
|AWS_ACCESS_KEY|accessKey|
|AWS_LAMBDA_FUNCTION_NAME|SystemInfo|
|context|lambdainternal.api.LambdaContext@604ed9f0|

アクセスキー等の情報も環境変数に乗っていましたのでそこはマスクしてます。そういう仕様だということは理解しておくべきかもしれません。この辺のキーを使ってAWS API呼び出したりするのかな？あと、ちゃんと設定した環境変数も出て来てます（あたりまえですが）。
OpenJDK on Amazon Linuxで動かしているみたいですね。こればっかりは実際に本稼働したときにどうなるかわかりませんけれども。あくまでこのテスト実行時はこうでした、というだけです。なんといってもサーバーレスですので、繰り返しになりますが実行環境（HW、OS、MW等々）はどうでもいいです。というか、どうでもいい前提でコードを書いてください、というのがLambda的な使い方と認識しました。

# 参考
[Lambda 関数ハンドラー(Java) - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model-handler-types.html)