軽量フレームワークMicronautでサーバーレスJavaを始めよう!


サーバーレスがとても盛り上がっていますね。
Javaはいわゆるコールドスタートの問題でサーバーレスには向いていないとの声もありましたが、今回紹介するMicronautを始めとしたマイクロフレームワークや、GraalVMのネイティブイメージ作成機能(SubstrateVM)の登場により状況が大きく変わってきています。

この辺りの流れは、@_kenshさんや@jyukutyoさんの「JJUG CCC 2019 Fall」での素晴らしい講演資料がありますので、そちらを見てワクワクしましょう。

さあ、ではMicronautで実際にサーバーレスJavaを作ってみましょう!

なんでMicronaut?

Micronautは、キャッチコピーの中で「サーバーレスのためのフレームワーク」であることを謳っています。
また「re:Invent2019」にて、MicronautとLambdaについてのセッションも開催していました。
他のフレームワークと比較して特にサーバーレスに力を入れていて、個人的にとても魅力を感じたので今回セレクトしました。

他のフレームワークはないの?

Java(を始めとするJVM言語)が使用できるマイクロフレームワークとしてメジャーなのは、下記3つかと思います。

どれもこれからが楽しみなフレームワークですね。
キャッチコピーなどを比較してみたので、ご参考までにどうぞ。

キャッチコピー

フレームワーク キャッチコピー 意訳
Micronaut A modern, JVM-based, full-stack framework for building modular, easily testable microservice and serverless applications. マイクロサービスとサーバーレスのためのモダンなフルスタックフレームワークだよ
Quarkus A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM. OpenJDK HotSpotとGraalVMに最適化したKubernetesネイティブなフレームワークだよ
Helidon Lightweigt. Fast. Crafted for Microservices. マイクロサービスのために作られた軽量で高速なフレームワークだよ

Googleトレンド(アメリカ、過去1年間)

GitHubスター数


https://www.githubcompare.com/

その他

  • 2019/11/28にリリースされたIntelliJ IDEA 2019.3では、「Microservices frameworks support」としてMicronaut、Quarkus、Helidonの3つがサポートされました。注目度合いが伺えますね!
  • Spring Frameworkも次期バージョンの5.3でGraalVMに対応するようです。こちらも楽しみ!

Micronautのセットアップ

では、Micronautをセットアップしてみましょう。
公式サイトではSDKMANでのインストールが推奨されているので、その手順でインストールしましょう。

SDKMANのインストール

$ curl -s https://get.sdkman.io | bash

ターミナル再起動、もしくは下記シェルを実行

$ source "$HOME/.sdkman/bin/sdkman-init.sh"

sdkコマンドが通ればOK

$ sdk version
SDKMAN 5.7.4+362

Micronautのインストール

$ sdk install micronaut

mnコマンドが通ればOK

$ mn -V
| Micronaut Version: 1.2.7
| JVM Version: 11.0.5

MicronautでのLambda関数の作成

プロジェクトの雛形はmnコマンド経由で作成します。
mnだけを入力すると対話モードで起動できます。
コマンド補完もできるので便利です。コマンド履歴も辿れます。

$ mn
| Starting interactive mode...
| Enter a command name to run. Use TAB for completion:
mn> 

コマンドの使い方はmn> helpで確認できます。

mn> help
Usage: mn [-hnvVx] [COMMAND]
Micronaut CLI command line interface for generating projects and services.
Commonly used commands are:
  create-app NAME
  create-cli-app NAME
  create-federation NAME --services SERVICE_NAME[,SERVICE_NAME]...
  create-function NAME
・・・

今回はLambda関数を作りたいので、create-functionを使いましょう。

mn> create-function hello-world
| Generating Java project...
| Function created at /Users/cojohnny/micronaut/hello-world
| Initializing application. Please wait...

一度対話モードを抜けて、生成されたプロジェクトを確認します。

mn> exit
$ cd hello-world/
$ tree
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradle.properties
├── gradlew
├── gradlew.bat
├── micronaut-cli.yml
├── settings.gradle
└── src
    ├── main
    │   ├── java
    │   │   └── hello
    │   │       └── world
    │   │           ├── Application.java
    │   │           ├── HelloWorld.java
    │   │           └── HelloWorldFunction.java
    │   └── resources
    │       ├── application.yml
    │       └── logback.xml
    └── test
        └── java
            └── hello
                └── world
                    ├── HelloWorldClient.java
                    └── HelloWorldFunctionTest.java

生成されたソースを見てみましょう。
HelloWorldFunction.javaのapplyメソッドがLambda関数です。
今の実装は、入力されたJSONのnameの設定値を返すだけの処理になっています。

HelloWorldFunction.java
package hello.world;

import io.micronaut.function.executor.FunctionInitializer;
import io.micronaut.function.FunctionBean;
import javax.inject.*;
import java.io.IOException;
import java.util.function.Function;

@FunctionBean("hello-world")
public class HelloWorldFunction extends FunctionInitializer implements Function<HelloWorld, HelloWorld> {

    @Override
    public HelloWorld apply(HelloWorld msg) {
         return msg;
    }

    /**
     * This main method allows running the function as a CLI application using: echo '{}' | java -jar function.jar 
     * where the argument to echo is the JSON to be parsed.
     */
    public static void main(String...args) throws IOException {
        HelloWorldFunction function = new HelloWorldFunction();
        function.run(args, (context)-> function.apply(context.get(HelloWorld.class)));
    }    
}

ではgradlewでビルドしてみましょう。

$ ./gradlew assemble

Jarが2種類出来上がります。

$ tree build/libs/
build/libs/
├── hello-world-0.1-all.jar
└── hello-world-0.1.jar

ファイル名の末尾に-allとついているほうが、依存ライブラリが含まれた「Fat Jar」なので、そちらをLambdaにアップロードします。

Lambda関数のセットアップ

作成した「hello-world-0.1-all.jar」をAWSにアップロードしましょう。
Handlerには「hello.world.HelloWorldFunction::apply」を設定。

Lambda関数の実行

それではLambda関数を実行してみましょう。
Lambdaコンソール上部の「Test」ボタンから、リクエストのJSONを設定します。
JSONには下記を指定。

{
  "name": "Lambda"
}

「Test」実行!
動きましたね!

まとめ

今回は単純な処理の実行だけでしたが、気軽にフレームワークを含んだLambda関数を作成して実行できることがわかったと思います。
Micronautなどの軽量フレームワークを使うことで、フレームワークの豊富な機能を使いながらLambdaを構築することができるようになるため、大規模アプリケーションへのサーバーレスアーキテクチャの適用もますます拡がっていきそうですね。

一番の懸念だったコールドスタートについても、先日のre:Invent2019で発表された「provisioned concurrency」という有力な解決策が提示されたため、本番環境への適用に対するハードルもどんどん下がってきています。

2020年もさらなる拡がりが予想されるサーバーレスの世界、とても楽しみですね!
特にJavaの大規模システムに関わっていてサーバーレスにあまり馴染みがなかったという方も、今が始めどきだと思います。
一緒にサーバーレスJavaを始めましょう!

参考

Java最新フレームワーク、Helidon、Micronaut、Quarkusをnative-imageまでまとめて試す
Micronaut User Guide
AWS Lambda announces Provisioned Concurrency