Micronaut 2.x 入門 ~AWS Lambdaにネイティブビルドしてデプロイ~

前回: [Micronaut 2.x 入門](https://qiita.com/akkiyyy0114/items/27d74671763bedf76ffe)に続いて今回はネイティブビルドしたMicronautアプリをAWS Lambdaにアップロードしていきます。

versionが2.0以降初期で作成されるファンクションアプリが大きく変わったので少し混乱しましたが、公式のブログに細かく作り方が記載されていたので、それを元に作成していきます。


##  環境

* macOS Catalina: 10.15.4
* メモリ: 16GB
* Micronaut: 2.0.1
* Java: 11.0.8.j9-adpt
* Docker for Mac:  2.3.0.4
* AWSアカウント



## MicronautでLambda Function用アプリケーション作成

 作業用ディレクトリを作成

```bash
$ mkdir 02-native-function && cd 02-native-function
```

Micronaut  CLIを利用して作成していきます。今回は言語にJavaを使います。

```bash
# CLIでファンクションアプリを作成
$ mn create-function-app example.micronaut.complete --features=aws-lambda,graalvm

# 確認
$ tree complete
complete/
├── Dockerfile
├── README.md
├── bootstrap
├── build.gradle
├── deploy.sh
├── docker-build.sh
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── example
    │   │       └── micronaut
    │   │           ├── Book.java
    │   │           ├── BookLambdaRuntime.java
    │   │           ├── BookRequestHandler.java
    │   │           └── BookSaved.java
    │   └── resources
    │       ├── META-INF
    │       │   └── native-image
    │       │       └── example.micronaut
    │       │           └── complete-application
    │       │               └── native-image.properties
    │       ├── application.yml
    │       └── logback.xml
    └── test
        └── java
            └── example
                └── micronaut
                    └── BookRequestHandlerTest.java

16 directories, 21 files

# アプリケーションフォルダに移動
$ cd complete
```



### 作成されたファイルを見ていこう

`src/main/java/example/micronaut/` に下記4ファイルが作成されます。

* Book.java
* BookLambdaRuntime.java
* BookSaved.java
* BookRequestHandler.java

それでは、1つずつファイル内容を見ていきます。

`[Book.java]`
入力を保存するためのモデルクラスです。

```java:Book.java
package example.micronaut;
import edu.umd.cs.findbugs.annotations.NonNull;
import io.micronaut.core.annotation.Introspected;
import javax.validation.constraints.NotBlank;

@Introspected
public class Book {

    @NonNull
    @NotBlank
    private String name;

    public Book() {
    }

    @NonNull
    public String getName() {
        return name;
    }

    public void setName(@NonNull String name) {
        this.name = name;
    }
}
```




* `java:BookLambdaRuntime.java`  
カスタムランタイムを実装するためのクラスです。

```java:BookLambdaRuntime.java
package example.micronaut;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import io.micronaut.function.aws.runtime.AbstractMicronautLambdaRuntime;
import java.net.MalformedURLException;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import edu.umd.cs.findbugs.annotations.Nullable;

public class BookLambdaRuntime extends AbstractMicronautLambdaRuntime<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent, Book, BookSaved> {

    public static void main(String[] args) {
        try {
            new BookLambdaRuntime().run(args);

        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
    }

    @Override
    @Nullable
    protected RequestHandler<Book, BookSaved> createRequestHandler(String... args) {
        return new BookRequestHandler();
    }
}
```

いまいち筆者も良くわかっておりませんが、ソースコードの中身を見るにAPIGatewayからのLambda呼び出しをするための受付用のクラスのようなものでしょうか。
S3フックイベントのLambdaを作るときは `AbstractMicronautLambdaRuntime` のジェネリクスがS3用になるのではないでしょうか。


*  `[java:BookSaved.java]`  
戻り値を保存するためのモデルクラスです。

```java:BookSaved.java
package native.lambda
import io.micronaut.core.annotation.Introspected

@Introspected
class BookSaved {
    var name: String? = null
    var isbn: String? = null
}
```


* `[java:BookRequestHandler.java]`  
メインの処理を行うクラスです。

```java:BookRequestHandler.java
package example.micronaut;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.function.aws.MicronautRequestHandler;
import java.util.UUID;

@Introspected
public class BookRequestHandler extends MicronautRequestHandler<Book, BookSaved> {

    @Override
    public BookSaved execute(Book input) {
        BookSaved bookSaved = new BookSaved();
        bookSaved.setName(input.getName());
        bookSaved.setIsbn(UUID.randomUUID().toString());
        return bookSaved;
    }
}
```

APIの入力値を `Book.java` で値を受け取り、戻り値を `BookSaved.java` に詰めて返却します。



## Lambda関数の作成

Micronautで作成したアプリケーションを上げるための準備を進めていきます。



### 1. AWSコンソールにログイン

[AWSコンソール](https://aws.amazon.com/jp/)にログインしましょう。
もしまだ未作成の場合は、AWSアカウントの作成を行なってから再度こちらに戻ってきてください。



### 2. Lambda作成

ログイン後に `サービス > Lambda` を開きましょう。
画面右上の[関数の作成]を押します。

以下のように選択と入力をしましょう。
![create-lambda-function.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/b622b144-a089-1181-d098-c6e6fd26dda1.png)
MicronautはJavaのアプリですが、ネイティブビルドしたアプリケーションをあげるため、ランタイムはJavaではなく「独自のブートストラップ」を選択しましょう。


### 基本設定の修正

Lambda作成後の画面で基本設定を編集していきます。
![lambda-info-setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/0bafe1cf-6f13-07df-8a63-a7dc886517b3.png)


右上の編集ボタンから以下のように変更します。

* ハンドラ: example.micronaut.BookRequestHandler
* メモリ: 512MB

![lambda-info-setting-datail.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/104fa228-0e12-0c41-b509-596978ed2bfd.png)
保存します。


## ネイティブイメージの作成

CLIで作成する際に `--features=graalvm` をつけたことで、ネイティブビルド用のファイルが作成されています。

```dockerfile:Dockerfile
FROM gradle:6.3.0-jdk11 as builder
COPY --chown=gradle:gradle . /home/application
WORKDIR /home/application
RUN ./gradlew build --no-daemon
FROM amazonlinux:2018.03.0.20191014.0 as graalvm

ENV LANG=en_US.UTF-8

RUN yum install -y gcc gcc-c++ libc6-dev  zlib1g-dev curl bash zlib zlib-devel zip

ENV GRAAL_VERSION 20.1.0
ENV JDK_VERSION java11
ENV GRAAL_FILENAME graalvm-ce-${JDK_VERSION}-linux-amd64-${GRAAL_VERSION}.tar.gz

RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o /tmp/${GRAAL_FILENAME}

RUN tar -zxvf /tmp/${GRAAL_FILENAME} -C /tmp \
    && mv /tmp/graalvm-ce-${JDK_VERSION}-${GRAAL_VERSION} /usr/lib/graalvm

RUN rm -rf /tmp/*
CMD ["/usr/lib/graalvm/bin/native-image"]

FROM graalvm
COPY --from=builder /home/application/ /home/application/
WORKDIR /home/application
RUN /usr/lib/graalvm/bin/gu install native-image
RUN /usr/lib/graalvm/bin/native-image --no-server -cp build/libs/complete-*-all.jar
RUN chmod 777 bootstrap
RUN chmod 777 complete
RUN zip -j function.zip bootstrap complete
EXPOSE 8080
ENTRYPOINT ["/home/application/complete"]
```

```shell:deploy.sh
#!/bin/bash
docker build . -t complete
mkdir -p build
docker run --rm --entrypoint cat complete  /home/application/function.zip > build/function.zip
```



### デプロイ用のファイルを作成

`Dockerfile` を動かす `deploy.sh`を使ってネイティブビルド用の成果物を作成していきます。
(筆者のネットワークの問題なのかもしれませんが、非常に時間がかかりました...。)

```bash
$ sh ./deploy.sh
Sending build context to Docker daemon  17.63MB
Step 1/24 : FROM gradle:6.3.0-jdk11 as builder
 ---> 0290cb9c9a7b
Step 2/24 : COPY --chown=gradle:gradle . /home/application
 ---> 287bbae39066
 ...
 
 # ファイルはbuild配下に作成されます
 $ ls build
 function.zip
```



## Lambdaにアップロード

デプロイされたファイルをLambdaにアップロードしていきます。
上で作成したLambda関数を開きましょう。



[関数コード] のアクションを押して、「.zipファイルをアップロード」を押して、先ほど作成した `function.zip` を選択してアップロードしてください。
![lambda-upload.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/f3d3c7de-ca1c-6458-69db-2f9f5715abe5.png)


## Lambdaを動かしてみよう

まずは動かすためのテストイベントを作成します。
上の「テストイベントの設定」を押して以下のように設定します。



```json:bookTest
{
  "body": "{\"name\":\"Book Test!\"}"
}
```
![test-event.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/b6a2904e-e1b6-8abc-6542-846100d505ce.png)
設定が完了したら「テスト」を押します。
一番下のログ出力の結果を見てみましょう。
![mn-lambda-native.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/d0004372-d455-a56b-d95b-fd38dab92e5e.png)
**Init Duration: 450.70 ms**
**Duration: 189.76 ms**

Init Durationは起動時間で、Durationは処理時間です。
起動時間が 0.45秒なので非常に速いことがわかります。

コールドスタートであるJavaを元に作成したアプリケーションでこの時間になるのは感動的ですね！



## おまけ

ネイティブビルドではなく、通常のJarファイルでデプロイしたアプリケーションでの実行速度を見てみましょう。
作成してアップロードするところは省略します。
![mn-lambda-normal.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/701003/8c33605d-2728-b797-6be8-07fc75b77624.png)
**Init Duration: 2909.91 ms**
初期起動時間は約3秒です。

ネイティブビルドとの差は**約2.5秒**ほどあります。




## まとめ

簡単にAWS Lambdaまであげれることが確認できました！
CLI経由でアプリケーションを作成するだけで `Dockerfile` やアップロード用の `deploy.sh` が生成されるのでデプロイファイルもお手軽に作成できますね。
起動速度も通常のJavaで動かすよりはるかに速いことが体験できました。



次回はMicronautでDBへ接続して処理するようなWebアプリケーションを作成していきたいと思います。



## 参考文献

この記事は以下の情報を参考にして執筆しました。

- [MICRONAUT 2: AWS LAMBDA FUNCTIONS](https://micronaut.io/blog/2020-08-31-micronaut-2-aws-lambda.html)
- [Deploy a Micronaut function as a GraalVM Native Image to AWS Lambda](https://guides.micronaut.io/mn-serverless-function-aws-lambda-graalvm/guide/index.html)