AWS LambdaにデプロイしたJavaアプリケーションでユーザー定義システムプロパティを参照する

以下のQuarkusを利用したプロジェクトのソースコードを読んでいてなるほど、と思ったので調べたことをまとめておく。

 

 

[Javaシステムプロパティ]

Javaにはシステムプロパティと呼ばれるJavaの動作環境に関する情報がある。

 

// 利用例
System.out.println(System.getProperty("java.version")); //1.8.0_272
System.setProperty("myKey", "hoge");
System.out.println(System.getProperty("myKey")); //hoge

 

デフォルトでJavaのバージョン情報を取得したりできるのに加えて、上記のように利用者が独自にプロパティを設定することもできる。

 

通常はソースコード上でプロパティを上書きして設定することは少なく、Javaアプリケーションを起動する際に、javaコマンドのオプションとして-Dkey=valueの形式で設定する。たとえばこんな風に。

kdnakt.hatenablog.com

  

[AWS Lambdaと環境変数]

AWS Lambdaでは、ランタイムとしてJava 8またはJava 11を選択することができる。しかし、AWS Lambdaでは利用者が変更できる設定値として用意されているのは環境変数であって、システムプロパティではない。

 

環境変数の場合はSystem.getenv("ENV_VAR_KEY")のように取得することができるが、System.setenv()メソッドは用意されていないため、環境変数Javaソースコード上では変更することができない。一方、システムプロパティの場合は、ソースコード上で値を変更することができるので、設定を変更した場合のテストコードを書くのがそれほど難しくはない。

 

環境変数に依存するテストコードを書く際は、環境変数の取得部分を外に出して、テスト対象のメソッドに引数として環境変数を渡すか、staticメソッドであるSystem.getenv(String)をモックするためにPowerMockなどのライブラリに頼る、といった方法が必要であった。

 

テストが書きづらいという問題はあるものの、AWS Lambda + Javaで開発環境と本番環境の動作を変更する、といった場合にはAWS Lambdaの環境変数を利用することになる。そう思っていたのだが……。

 

[環境変数JAVA_TOOL_OPTIONS]

QuarkusをServerless Frameworkと組み合わせてLambdaに乗せるのにいい感じの例がないかな、と思ってGitHubを漁っていたときにこちらのリポジトリを見つけた。

 

serverless.yml環境変数の設定箇所をみると次のように書かれていた。

environment:
  JAVA_TOOL_OPTIONS: -Dquarkus.profile=${opt:stage, self:provider.stage} -Dquarkus.lambda.handler=${self:custom.function.addHandler} -Ddynamo.table.name=${self:custom.dynamodb.tableName} -Dquarkus.dynamodb.aws.region=${self:provider.region}

 

-Dkey=valueの形式で設定しているからにはこれはやはりシステムプロパティなのだろう、と推測できたが、JAVA_TOOL_OPTIONSという環境変数名には馴染みがなかった。このリポジトリで追加されている何か特有のライブラリでしか使えないのだろうか、と懸念しつつGoogle先生に尋ねると、Oracleのドキュメントがヒットした。

 

どうやら、本来はJavaエージェント*1の設定などのために利用する環境変数のようだ。

環境変数以外にも、システムプロパティを設定するのにも使える、ということらしい。

 

複数のシステムプロパティを設定できるようだが、この環境変数の上限は1024文字までらしいので、その点は注意が必要そうだ。AWS Lambdaの環境変数自体は関数1つにつき合計で4KBまで許容されているが、JVMの(正確にはAWS LambdaのJava 8ランタイムであるOpenJDKの)側で制約がある、ということらしい。

 

ただし、AWS LambdaのJava 11ランタイムはOpenJDKではなくAmazon Correttoを使用している。

こちらのソースコードを確認したところ、JAVA_TOOL_OPTIONS環境変数をパースする際のバッファの指定がなくなっていたので、もしかしたらJava 11の場合は1024文字以上指定できるのかもしれない(未検証)*2

 

[AWS Lambda (Java 8)の動作確認]

念のため、簡単なソースを書いて動作確認をしておく。作業ディレクトリで以下のコマンドを実行して、Serverless FrameworkのJavaプロジェクトを作成する。

sls create -t aws-java-maven -p java-envvar

 

つぎに、serverless.ymlに以下のように環境変数の設定を追加する。

service: java-envvar

provider:
  name: aws
  runtime: java8
  region: ap-northeast-1
  memorySize: 256

package:
  artifact: target/hello-dev.jar

functions:
  hello:
    handler: com.serverless.Handler
    # ここから環境変数の設定を追加
    environment:
      STAGE: dev
      JAVA_TOOL_OPTIONS: -Dapp.name=exampleApp

 

そして、Lambda関数のコードにシステムプロパティ(この場合はapp.name)を取得するコードを追加する。

@Override
public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
	input.put("System::STAGE", System.getenv("STAGE")); // 環境変数を取得してMapに詰める
	input.put("System::appName", System.getProperty("app.name")); // システムプロパティを取得してMapに詰める
	Response responseBody = new Response("success", input);
	return ApiGatewayResponse.builder()
		.setStatusCode(200)
	    .setObjectBody(responseBody)
		.build();
}

 

mvn installコマンドでjarファイルをビルドしたあとは、sls deployコマンドでLambdaにjarファイルをデプロイする。sls invokeコマンドでLambda関数を呼び出して動作確認をすると、下のようにserverless.ymlで環境変数JAVA_TOOL_OPTIONSに指定したシステムプロパティを取得することができた。

$ sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\"message\":\"success\",\"input\":{\"System::STAGE\":\"dev\",\"System::appName\":\"exampleApp\"}}",
    "isBase64Encoded": false
}

 

serverless.ymlにさらにシステムプロパティを追加して試してみる。

functions:
  hello:
    handler: com.serverless.Handler
    environment:
      STAGE: dev
      JAVA_TOOL_OPTIONS:
        -Dapp.name=exampleApp2
        -Dapp.version=3

 

追加したシステムプロパティを出力できるように関数コードを以下のように修正する。

@Override
public ApiGatewayResponse handleRequest(Map<String, Object> input, Context context) {
	input.put("System::STAGE", System.getenv("STAGE"));
	input.put("System::appName", System.getProperty("app.name"));
	input.put("System::appVersion", System.getProperty("app.version")); // この行を追加
	Response responseBody = new Response("success", input);
	return ApiGatewayResponse.builder()
		.setStatusCode(200)
	    .setObjectBody(responseBody)
		.build();
}

 

ビルド、デプロイ、そしてLambda関数を呼び出すと以下の結果が得られた。問題なく複数のシステムプロパティの値を取得することができた。

$ sls invoke -f hello
{
    "statusCode": 200,
    "body": "{\"message\":\"success\",\"input\":{\"System::STAGE\":\"dev\",\"System::appName\":\"exampleApp2\",\"System::appVersion\":\"3\"}}",
    "isBase64Encoded": false
}

 

[まとめ]

 

*1:監視のためのAppDynamicsとかコードカバレッジ測定のためのjacocoあたりが有名だろう。

*2:そもそもCorrettoはOpenJDKのAmazonディストリビューションなので、OpenJDKでも同じはず、と思ったらその通りだった。http://hg.openjdk.java.net/jdk/jdk11/file/1ddf9a99e4ad/src/hotspot/share/runtime/arguments.cpp#l3284