前提条件
初心者向け。Lambdaは知ってるけどWeb APIどうやって実装するんだろう、といったところから。
先に結論を書いておくと、Web APIなLambda関数をJavaで実装するのは非常に燃費が悪い感があるのでオススメできない。制約が無い限りは、node.jsとかpythonとかを使った方が断然楽だと思った。
※トラブルシューティングの面でも、Javaとその他のスクリプト言語では情報量が全然違うというか、その他のスクリプト言語ってたぶんJavaほどトラブらない気がする……
一応、環境はWindows10+Eclipse+Mavenなので、環境構築からする場合はこの辺の過去記事を見つつセットアップすると導入が早い。
あと、JDKは8か11にしておく。Lambdaは2020年4月時点ではまだJDK13のランタイムに対応していないのである。
サンプルプロジェクトからJarファイルを作成する
Eclipseのメニューで、ファイル→新規→プロジェクトのダイアログで、AWS Lambda Java Projectを選択。
プロジェクト名やMaven configurationは適当に決めて、Lambda Function Handlerの入力タイプでCustomを選択して完了ボタンを押すと、Lambda関数のサンプルプロジェクトが作られる。
サンプルプロジェクトにはJUnitのテストコードの雛形も入っているので便利!
Web APIを実装する場合はこのカスタムハンドラをいじっていくのが手っ取り早いっぽい。
ハンドラの詳細な紹介は以下。
【AWS公式】AWS Lambda における Java 関数ハンドラーの提供インターフェイスの使用
RequestHandlerのインプットとアウトプットを作り込んでいけば良い。
↑のハンドラのサンプルをベースにコードを書いたらプロジェクトを右クリックして、
基本はこれでtargetフォルダ配下にjarファイルができてアップロードして動かせるはずなのだけど、この後の工程でClassNotFoundException
が出て上手くいかない場合は、以下を参考にしながらmaven-shade-pluginを使ったパッケージ作成を行う(キャプチャの「Mavenビルド...」のダイアログで、ゴールをpackage shade:shade
指定する)。
【AWS公式】Java Lambda 関数を使用した「ClassNotFoundException」エラーのトラブルシューティング方法を教えてください。
【AWS公式】Eclipse を使用した .jar デプロイパッケージの作成
AWS SAM Localは必要か?
SAM Localは、ローカル環境でLambda関数を実行したり、DynamoDBに接続したりできるもの。
今回くらいのお試しだと、SAM Localの起動時間のオーバーヘッドの方が大きいので素直にアップロードしてテスト入力してみる方が早いけど、単体試験で回帰テストやるつもりならあっても良いかも。それとてJUnitでも良い気はするけど……
参考は↓ここ。
【Developers.IO】[新ツール]AWS SAMをローカル環境で実行できるSAM Localがベータリリース
でも罠があって、npmでインストールできるSAM Localはバージョンが古くて、Java11のランタイムに対応していない。Java11ランタイムで動かしたいのであれば、SAM CLIを使う。
Docker Toolboxでインストールしたsam.exeのパスと、↑の手順でインストールしたsam.cmdのパスが違うので、設定を変えておくのを忘れないように。先にインストールしたからか、自分の環境ではDocker Toolboxのパスが優先されてしまうようだ。
Eclipseのメニューのウィンドウ→設定でダイアログを開き、AWSツールキット→AWS SAM Localでパスを設定できる。
前節に書いたClassNotFoundExceptionはここでも起きるので、出た場合は↑のmaven-shade-pluginを試してみる。
また、ローカルのDockerサーバに接続できないようなエラーが出る場合は、環境変数の設定が必要かもしれない。この辺を試しているときには色々なエラーが出ていて、何が効果があったのかが良く分からなくなってしまった。↓参考までに環境変数に関する説明。
【MAGELLAN Dev Center】Docker Toolbox の使い方
Lambda関数の作成
さて、ここまでくればあとは普通にLambda関数を作るだけ。
「一から作成」で、ランタイムをJava8か11か使っている方にして、適切なロール(S3とCloudWatchLogsにアクセスできるもの)を設定する。
↑で作成したMavenのパッケージであればzipする必要はないので、そのままS3にアップロードして、「Amazon S3からのファイルのアップロード」でJarファイルのURLを貼り付ける。
ハンドラは、サンプルそのままであるならcom.amazonaws.lambda.demo.LambdaFunctionHandler::handleRequest
を設定する。
[ファイルパス][ハンドラのクラスファイル名]::[ハンドラ]
な感じだ。
適当にテストを流して、期待した結果が得られたならファーストステップはクリアだ!
Amazon API Gateway/ALBのバックエンドで動かす
レスポンス編
で、ここからが問題。
どうやらAPI GatewayもALBも、定型のJSONをレスポンスしないと、Lambdaプロキシを通せなかったり、ALBのヘルスチェックでエラーになってしまう。
テストを通ったとしても、Web APIとしては実用できないのである。
【AWS公式】API ゲートウェイの「不正な Lambda プロキシ応答」エラー、または 502 ステータスコードを解決するにはどうすればよいですか?
【AWS公式】ターゲットとしての Lambda 関数
どちらにも書いてあるように、↓こんな感じのJSONである。
{
"isBase64Encoded": true|false,
"statusCode": httpStatusCode,
"headers": { "headerName": "headerValue", ... },
"body": "..."
}
しかもこれ、Lambda関数が良い感じにBodyだけ取り出しちゃったりするので、LambdaのテストやSAM Localじゃデバッグできない。API Gatewayの方は適当にPOJOのResponseオブジェクトを作ってみたら動いたが、ALBの方はよりstrictにチェックするらしく、皆目見当もつかない。
こんなのどうしたらええねん……と思って調べてみると、↓こんな記事が。
【cloudpack.media】JavaでLambda関数を書いてSAMでデプロイしてみた
今回はこれを参考に動かしてみて、Amazon API Gateway/ALBのどちらでも動作することが確認できた。
ちなみに、headers
でちゃんとContent-Typeを明示的に指定しない場合、API Gatewayは良い感じにapplication/jsonあたりを設定してくれるようだが、ALBはapplication/octet-streamを設定してしまう。Builderで以下のようにヘッダを設定しておく。
.setHeaders(new HashMap<String, String>(){{put("Content-Type","application/json");}})
リクエスト編
リクエストは、ALBもAPI Gatewayも、以下のページにあるようなJSONでリクエストハンドラに情報が渡ってくる。ALBにはパスパラメータを指し示す項目がないので、パスパラメータから情報を取得しないなら、おとなしくAPI Gatewayにするか、コンテナにしてSpringBootにしておけといったところか。
ちなみに、API Gatewayを使うにしても、パスパラメータを指定するには以下の様にリソースを作る際にパスパラメータであることを支持してあげないといけないので気を付けよう。
-
ALBの場合
【AWS公式】ターゲットとしての Lambda 関数 - ロードバランサーからのイベントの受け取り -
API Gatewayの場合
【AWS公式】API Gateway で Lambda プロキシ統合をセットアップする - プロキシ統合用の Lambda 関数の入力形式
アプリ内でごちゃごちゃとイベントを取得してJSONにして返してみると、以下の様に情報が渡ってくるのが分かった。
$ curl -X GET http://[ALBのドメイン]/LambdaTest | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"queryStringParameters": "{}"
}
$ curl -X GET http://[ALBのドメイン]/LambdaTest/11111 | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest/11111",
"queryStringParameters": "{}"
}
$ curl -X GET http://[ALBのドメイン]/LambdaTest?id=11111 | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"queryStringParameters": "{\"id\":\"11111\"}"
}
$ curl -X PUT -H 'Content-Type:application/json' -d "{\"id\":\"11111\"}" http://[ALBのドメイン]/LambdaTest | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"queryStringParameters": "{}",
"body": "{\"id\":\"11111\"}"
}
$ curl -X GET https://[API Gatewayのドメイン]/default/LambdaTest | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"pathParameters": "null",
"queryStringParameters": "null",
"body": "null"
}
$ curl -X GET https://[API Gatewayのドメイン]/default/LambdaTest/11111 | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest/11111",
"pathParameters": "{\"id\":\"11111\"}",
"queryStringParameters": "null",
"body": "null"
}
$ curl -X GET https://[API Gatewayのドメイン]/default/LambdaTest?id=11111 | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"pathParameters": "null",
"queryStringParameters": "{\"id\":\"11111\"}",
"body": "null"
}
$ curl -X PUT -H 'Content-Type:application/json' -d "{\"id\":\"11111\"}" https://[API Gatewayのドメイン]/default/LambdaTest | jq -r .
{
"greetings": "Hello from Lambda Ver 1.13",
"path": "/LambdaTest",
"pathParameters": "null",
"queryStringParameters": "null",
"body": "{\"id\":\"11111\"}"
}
サマライズすると以下のような感じになる。
構成要素 | ALB | API Gateway |
---|---|---|
path | String型。必ず何かしらの値が入る。 | String型。必ず何かしらの値が入る。 |
pathParameters | ALBのリクエストには存在しない構成要素。 | Object型。指定しない場合は明示的にnullが指定。 |
queryStringParameters | Object型。指定しない場合は明示的に"{}"が指定。 | Object型。指定しない場合は明示的にnullが指定。 |
body | String型。指定しない場合は空文字列。 | String型。指定しない場合は明示的にObject型でnullが指定。 |
こうは書いてみたものの、ALBから渡ってくるイベント情報ってあんまりちゃんとしたJSONじゃないんだよな……
received: {requestContext={elb={targetGroupArn=[ターゲットグループのARN]}}, httpMethod=PUT, path=/LambdaTest, queryStringParameters={}, headers={accept=*/*, content-length=14, content-type=application/json, host=[ALBのドメイン], user-agent=curl/7.61.1, x-amzn-trace-id=Root=xxxxxxxxxx x-forwarded-for=xxx.xxx.xxx.xxx, x-forwarded-port=80, x-forwarded-proto=http}, body={"id":"11111"}, isBase64Encoded=false}
互換性を求めないのであれば、API Gatewayの場合は素直にイベントハンドラのイベント情報をAPIGatewayProxyRequestEvent
にしておくのが無難な気がする。
おわりに
さて、↓次回は応用編。このアプリケーションでALB+LambdaのCI/CDパイプラインを作ってみよう。
ALBのバックエンドで動作するJava実装のLambda関数をBlue/GreenデプロイメントするCodePipelineを作る