はじめに
これまで、AWS Lambdaを主にPython、時々、Node.jsで作ってきましたが、気分転換にJavaでの開発方法を調べたのでまとめます。
今回は、Javaで書いたLambda関数のソースコードをGradleでビルドしてAWS SAMでデプロイしてみました。
Eclipseでの開発用に AWS Toolkit for Eclipse も提供されていますが、普段はVimで開発してCLIでデプロイしているので、そちらに合わせます。
検証環境
- OS: macOS High Sierra 10.13.6
- aws-cli: 1.15.38
- aws-sam: 0.11.0
- Java 1.8.0_221
- JDK: OpenJDK 1.8.0_212 (Amazon Corretto 8.212)
- Gradle 5.5.1
JavaでLambdaを開発する上でのポイント
ランタイムはJava 8のみ ランタイムはJava8とJava11 (2019/11/19 変更)
下の公式ドキュメントにあるように、提供されているJavaのランタイムはJava 8
(JDK 8
)のみです。
それ以外のバージョンを使用する場合は、サポートされるまで待つかカスタムランタイムを使う必要があります。
2019/11/19にJava 11が追加され、ランタイムは Java 8 (JDK 8) と Java 11 (JDK 11) から選べるようになりました。
※ 本記事に記載されるサンプルコードはJava 8の想定で書かれてます。
[Java による Lambda 関数のビルド - AWS Lambda] (https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model.html)
zipファイルかjarファイルにしてでデプロイ
Javaで書かれたアプリケーションを、zipファイルまたはスタンドアロンjarにパッケージしてデプロイします。
公式ドキュメントではMavenを使ってスタンドアロンjarに、Gradleを使ってzipファイルにしてデプロイする方法が紹介されてます。
今回は、Gradleでzipファイルにパッケージングしてデプロイする方法で試しました。
Java の AWS Lambda デプロイパッケージ - AWS Lambda
ハンドラー関数のリクエストの受け方・返し方が3種類
ハンドラー関数のリクエストの受け取り方、そして、レスポンスの返し方に、以下のように複数の方式があります。
- Java のシンプルな型
- POJO (Plain Old Java Object) 型
- ストリーム型
それぞれでハンドラー関数の書き方が変わります。つまり、ハンドラー関数の書き方に3種類あるということです。
「このイベントトリガーを使うならこのタイプ」というような決め方というよりも、どういったデータが送られてくるかという視点で使い分けるようです。
この状況にはこれという明確なものはないようなので、実際にJavaでLambda関数を開発する際はどの方式にするか要検討です。
今回は、「POJO型」を採用しました。
ハンドラーの入出力タイプ (Java) - AWS Lambda
今回作ったもの
Lambda関数のトリガーとして API Gateway
を使った、簡単なWebアプリケーションを作成しました。
POSTリクエストを送ると、"Hello"と返してくるだけのシンプルなAPIです。
サンプルコード
作成したコードはここに置いてます。
https://github.com/mmclsntr/awslambda-javagradle
プロジェクト構成
.
├── build/
│ ├── distributions/
│ │ └── awslambda-javagradle.zip # ビルドで生成されるデプロイパッケージ
│ └── ...
├── build.gradle # Gradleビルド設定ファイル
├── src/main/java/awslambda/javagradle
│ ├── Greeting.java # アプリの中核となる部分
│ ├── Handler.java # Lambdaのハンドラ関数を格納
│ └── Response.java # Lambdaのレスポンスを整形
└── template.yml # CloudFormationテンプレートファイル
└── その他Gradle用ファイル
コーディング
ハンドラクラス
package awslambda.javagradle;
import java.util.Collections;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
public class Handler implements RequestHandler<Map<String, Object>, Response> {
private static final Logger LOG = Logger.getLogger(Handler.class.getName());
@Override
public Response handleRequest(Map<String, Object> input, Context context) {
LOG.info("received: " + input);
LOG.setLevel(Level.INFO);
Greeting greetingBody = new Greeting("Hello");
return Response.builder()
.setStatusCode(200)
.setObjectBody(greetingBody)
.build();
}
}
レスポンスクラス
package awslambda.javagradle;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import org.apache.log4j.Logger;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Response {
private final int statusCode;
private final String body;
private final Map<String, String> headers;
private final boolean isBase64Encoded;
public Response(int statusCode, String body, Map<String, String> headers, boolean isBase64Encoded) {
this.statusCode = statusCode;
this.body = body;
this.headers = headers;
this.isBase64Encoded = isBase64Encoded;
}
...
}
あいさつクラス
サンプルとしてHelloと返してくれる超シンプルなアプリケーションを作ります。
package awslambda.javagradle;
public class Greeting {
private String greetings;
public Greeting(String greetings) {
this.greetings = greetings;
}
public String getGreetings() {
return greetings;
}
public void setGreetings(String greetings) {
this.greetings = greetings;
}
}
CloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
AWS Lambda Java with Gradle
Globals:
Function:
Timeout: 20
Resources:
PostGreetingFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: build/distributions/awslambda-javagradle.zip
Handler: awslambda.javagradle.Handler::handleRequest
Runtime: java8
Events:
GetOrder:
Type: Api
Properties:
Path: /
Method: post
Outputs:
ApiEndpoint:
Description: "API Gateway endpoint URL for Prod stage"
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
PostGreetingFunction:
Description: "PostGreeting Lambda Function ARN"
Value: !GetAtt PostGreetingFunction.Arn
デプロイ
ビルド設定ファイル作成
apply plugin: 'java'
repositories {
mavenCentral()
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
dependencies {
compile (
'com.amazonaws:aws-lambda-java-core:1.1.0',
'com.amazonaws:aws-lambda-java-log4j:1.0.0',
'com.amazonaws:aws-lambda-java-events:1.1.0',
'com.fasterxml.jackson.core:jackson-core:2.8.5',
'com.fasterxml.jackson.core:jackson-databind:2.8.5',
'com.fasterxml.jackson.core:jackson-annotations:2.8.5'
)
}
// Task for building the zip file for upload
task buildZip(type: Zip) {
from compileJava
from processResources
into('lib') {
from configurations.runtime
}
}
build.dependsOn buildZip
ビルド
Gradle コマンドでビルドします。
gradle build
AWS SAMでデプロイ
デプロイ先S3バケット作成
aws s3 mb s3://<デプロイ先S3バケット> --aws-profile=<AWSプロファイル>
パッケージ
sam package
で、上で作成したS3バケットへ実行ファイルをアップロード & デプロイ用テンプレートファイルを生成します。
sam package \
--s3-bucket <デプロイ先S3バケット名> \
--s3-prefix <デプロイ先S3フォルダ名(プレフィクス) ※任意> \
--output-template-file output.yml \
--profile <AWSプロファイル>
アウトプットとして、 output.yml
ファイルが作られます。
デプロイ
sam deploy
で、LambdaとAPI Gatewayをデプロイします。
sam deploy \
--template-file output.yml \
--stack-name awslambda-javagradle-greeting \
--capabilities CAPABILITY_IAM \
--profile <AWSプロファイル>
確認
AWS上にLambda関数とAPI Gatewayが作られたので、エンドポイントにPOSTリクエストを投げて見ます。
curl -X POST https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
{"greetings":"Hello"}
所感
私自身、普段はJavaを使わないのでちょっと時間がかかりましたが、簡単に作れたなという印象です。Pythonなどのスクリプト言語で作るよりも当然ながらコード量は増えますが、厳格なサーバーレス開発ができることは魅力的です。
ハンドラー関数の書き方に複数種類あるといったJavaで書く上での特有の仕様が、今回だけでは把握しきれませんでした。実際に使う際は、その辺りも考慮しながら詳細設計したいと思います。
また、噂通り、初期起動が遅いです (レスポンスされるまで5秒くらい)。
参照: https://cero-t.hatenadiary.jp/entry/20160106/1452090214
まとめ
Javaを利用したLambda関数の開発の特徴やコーディング感覚をつかむことができました。
もし、サーブレットから移行するならそこそこ大規模な改修が必要と思いますが、代わりとして使えなくもない印象です。
ランタイムがこのままJava8のみなのか、そこのサポートが少し心配です。。
API Gatewayをトリガーとしましたが、他のサービスも試して見たいと思います。
参考
https://qiita.com/kamata1729/items/8d88ea10dd3bb61fa6cc
https://qiita.com/riversun/items/7fcc06617b469aed8f27