Help us understand the problem. What is going on with this article?

Amazon API Gateway/ALBのバックエンドで動くLambda関数をJava(Eclipse+maven)で実装する

前提条件

初心者向け。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のインプットとアウトプットを作り込んでいけば良い。

↑のハンドラのサンプルをベースにコードを書いたらプロジェクトを右クリックして、
- Maven clean
- Maven install
する。分かりにくいのでキャプチャを貼っておくと、↓こんな感じ。
キャプチャ.PNG

基本はこれで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を使う。

【AWS公式】AWS SAM CLI のインストール

Docker Toolboxでインストールしたsam.exeのパスと、↑の手順でインストールしたsam.cmdのパスが違うので、設定を変えておくのを忘れないように。先にインストールしたからか、自分の環境ではDocker Toolboxのパスが優先されてしまうようだ。
Eclipseのメニューのウィンドウ→設定でダイアログを開き、AWSツールキット→AWS SAM Localでパスを設定できる。

キャプチャ2.PNG

前節に書いた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を使うにしても、パスパラメータを指定するには以下の様にリソースを作る際にパスパラメータであることを支持してあげないといけないので気を付けよう。

キャプチャ.PNG

アプリ内でごちゃごちゃとイベントを取得してJSONにして返してみると、以下の様に情報が渡ってくるのが分かった。

ALB① パラメータ指定なし
$ curl -X GET http://[ALBのドメイン]/LambdaTest | jq -r .
{
  "greetings": "Hello from Lambda Ver 1.13",
  "path": "/LambdaTest",
  "queryStringParameters": "{}"
}
ALB② パスパラメータを指定
$ curl -X GET http://[ALBのドメイン]/LambdaTest/11111 | jq -r .
{
  "greetings": "Hello from Lambda Ver 1.13",
  "path": "/LambdaTest/11111",
  "queryStringParameters": "{}"
}
ALB③ クエリパラメータを指定
$ curl -X GET http://[ALBのドメイン]/LambdaTest?id=11111 | jq -r .
{
  "greetings": "Hello from Lambda Ver 1.13",
  "path": "/LambdaTest",
  "queryStringParameters": "{\"id\":\"11111\"}"
}
ALB④ リクエストボディでパラメータを渡す
$ 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\"}"
}
API Gateway① パラメータ指定なし
$ 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"
}
API Gateway② パスパラメータを指定
$ 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"
}
API Gateway③ クエリパラメータを指定
$ 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"
}
API Gateway④ リクエストボディでパラメータを渡す
$ 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を作る

その他

文字コードに関するwarningがうるさいときに

↓これ読んだ。
【情報科学屋さんを目指す人のメモ】Eclipse+Maven:「Using platform encoding (MS932 actually) to copy filtered resources, …」エラーの対策方法

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away