Java
AWS
lambda

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

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

Lambdaとは?

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

  • ファイルストレージサービスであるところのS3(もはや単なるストレージの域を超越していますが)にファイルがアップロードされた
  • メッセージがキューにputされた
  • API Gatewayにリクエストがきた
  • EC2インスタンスが起動した
  • 3時になった
  • おなかがすいた(と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ファイルをビルドパスに追加する必要があります。1

作る

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

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はテスト用です。1個目の引数こそ本来は大事なんでしょうけど今回は何もしません。

乗せる

AWSコンソールを開いてLambdaの関数を作ります(関数という単位で動きます。複数の関数をオーケストレーションするサービスもあるようです(詳細未調査))。
スクリーンショット 2018-01-31 21.39.16.png
適当に名前とランタイム(今回はJava8)を選んで「関数の作成」を押します。
標準出力はCloudWatchLogsへ流れるので、事前にCloudWatchLogsへのWrite権限のあるロールを作って必要に応じてここでアタッチしてください。
スクリーンショット 2018-01-31 21.39.50.png
スクリーンショット 2018-01-31 21.39.57.png
本来ならここでトリガーを選んで云々となりますが、とにかくテストしてみたいだけなので、その辺の条件だけいれます。
スクリーンショット 2018-01-31 21.40.05.png
関数コードのところで、「アップロード」からjarファイルをアップロード、大事なのが「ハンドラ」でここに実行するメソッドを入力します。書き方が決まっていて、"."表記でクラスのフルパスの後ろに"::"をつけてメソッド名です。
今回は"net.mognet.aws.lambda.SystemInfo::printSystemInfo"となります。ついでに環境変数もつけてみました。一旦「保存」すると実際にファイルがアップロードされます。
スクリーンショット 2018-01-31 21.41.03.png
次にテストの準備です。テストケース(入力設定=第一引数設定)です。画面上部の「テストイベントの設定」を選びます。
スクリーンショット 2018-01-31 21.41.19.png
実行するメソッドpublic static String printSystemInfoの第一引数がintなので、1とだけ書いて終わりです。下の方にある「保存」を押します。これでテスト準備完了です。

いざ実行!

おもむろに「テスト」を押します。
スクリーンショット 2018-01-31 21.42.15.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_DAEMONADDRESS 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_TRACEID 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_DAEMONPORT 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


  1. EclipseにはAWSのツールキットプラグインがあるので、この環境をセットアップしておくだけでも可です。