Edited at

JavaでLambda関数を書いてSAMでデプロイしてみた


はじめに

 これまで、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のみ

 下の公式ドキュメントにあるように、提供されているJavaのランタイムはJava 8 (JDK 8)のみです。

 それ以外のバージョンを使用する場合は、サポートされるまで待つかカスタムランタイムを使う必要があります。

Java による Lambda 関数のビルド - AWS Lambda


zipファイルかjarファイルにしてでデプロイ

 Javaで書かれたアプリケーションを、zipファイルまたはスタンドアロンjarにパッケージしてデプロイします。

 公式ドキュメントではMavenを使ってスタンドアロンjarに、Gradleを使ってzipファイルにしてデプロイする方法が紹介されてます。

 今回は、Gradleでzipファイルにパッケージングしてデプロイする方法で試しました。

Java の AWS Lambda デプロイパッケージ - AWS Lambda


ハンドラー関数のリクエストの受け方・返し方が3種類

 ハンドラー関数のリクエストの受け取り方、そして、レスポンスの返し方に、以下のように複数の方式があります。


  1. Java のシンプルな型

  2. POJO (Plain Old Java Object) 型

  3. ストリーム型

 それぞれでハンドラー関数の書き方が変わります。つまり、ハンドラー関数の書き方に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用ファイル


コーディング


ハンドラクラス


Handler.java

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();
}
}



レスポンスクラス


Response.java

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と返してくれる超シンプルなアプリケーションを作ります。


Greeting.java

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テンプレート


template.yml

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



デプロイ


ビルド設定ファイル作成


build.gradle

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