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

# はじめに

　これまで、AWS Lambdaを主にPython、時々、Node.jsで作ってきましたが、気分転換にJavaでの開発方法を調べたのでまとめます。

　今回は、Javaで書いたLambda関数のソースコードをGradleでビルドしてAWS SAMでデプロイしてみました。
　Eclipseでの開発用に [AWS Toolkit for Eclipse](https://docs.aws.amazon.com/ja_jp/toolkit-for-eclipse/v1/user-guide/welcome.html) も提供されていますが、普段は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](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-java-how-to-create-deployment-package.html)

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

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

1. Java のシンプルな型
2. POJO (Plain Old Java Object) 型
3. ストリーム型

　それぞれでハンドラー関数の書き方が変わります。つまり、ハンドラー関数の書き方に3種類あるということです。

　「このイベントトリガーを使うならこのタイプ」というような決め方というよりも、どういったデータが送られてくるかという視点で使い分けるようです。
　この状況にはこれという明確なものはないようなので、実際にJavaでLambda関数を開発する際はどの方式にするか要検討です。

　今回は、「POJO型」を採用しました。

[ハンドラーの入出力タイプ (Java) - AWS Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/java-programming-model-req-resp.html)

# 今回作ったもの

　Lambda関数のトリガーとして `API Gateway` を使った、簡単なWebアプリケーションを作成しました。
　POSTリクエストを送ると、"Hello"と返してくるだけのシンプルなAPIです。

# サンプルコード

　作成したコードはここに置いてます。
https://github.com/mmclsntr/awslambda-javagradle 

# プロジェクト構成

```text
.
├── 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用ファイル
```

# コーディング

## ハンドラクラス

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

## レスポンスクラス

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

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

```yml: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

```

# デプロイ
## ビルド設定ファイル作成

```gradle: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 コマンドでビルドします。

```bash
gradle build
```

## AWS SAMでデプロイ

### デプロイ先S3バケット作成

```bash
aws s3 mb s3://<デプロイ先S3バケット> --aws-profile=<AWSプロファイル>
```

### パッケージ

`sam package` で、上で作成したS3バケットへ実行ファイルをアップロード & デプロイ用テンプレートファイルを生成します。

```bash
sam package \
--s3-bucket <デプロイ先S3バケット名> \
--s3-prefix <デプロイ先S3フォルダ名(プレフィクス) ※任意> \
--output-template-file output.yml \
--profile <AWSプロファイル>
```

アウトプットとして、 `output.yml` ファイルが作られます。

### デプロイ

`sam deploy` で、LambdaとAPI Gatewayをデプロイします。

```bash
sam deploy \
--template-file output.yml \
--stack-name awslambda-javagradle-greeting \
--capabilities CAPABILITY_IAM \
--profile <AWSプロファイル>
```

## 確認

 AWS上にLambda関数とAPI Gatewayが作られたので、エンドポイントにPOSTリクエストを投げて見ます。

```bash:リクエスト
curl -X POST https://xxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/
```

```bash:レスポンス
{"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