Closed4

AmplifyでLambda(java)にgraalVMのnative-imageをDeployする(Micronaut)

tkykkndtkykknd

前提

  • 過去の(苦い)経験からインフラがスケールしやすいサーバレスでサービスを構築したい。
  • 既に歴史あるOSSがありそれらを利用するためにjavaを採用している
     (Scala,Kotlinでも論理的には良かったが、最終的に採用難になるのではとの懸念)
  • 効率化のためにAmplifyを採用
tkykkndtkykknd

課題

  • Amplidy×Lambda(Java)で業務アプリを開発。

  • しかしながら、初回の起動に10秒以上かかることが多かった。

  • そこで、プロビジョニングされた同時実行数を設定した

    • プロビジョニングされた同時実行設定を設定する
    • かつ、CloudWatchによるMetricsの監視を行い、6割以上使用率が増えた場合には同時実行設定数を増やす(AutoScale)
    • お片付けとして夜になると同時実行数を元に戻す。

    https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-concurrency.html

この方法論の問題

  1. 同時実行設定数の監視は数分おきであり、なだらかなスケーリングにならない。
  2. Lambda関数にてメモリ設定を上げ、高性能のインスタンスを割り当てると、同時実行数が多いと費用に大きく跳ねる。のでパフォーマンスチューニングの際の費用が一足飛びに上がるし、なんか勿体ない。
  3. 何と言っても確率としては低いもののcold startを引いてしまった時のUXが最悪。

なので全ての元凶であるjavaのLambda起動時問題は積年(1年立ってないから気持ち的には)の課題。

tkykkndtkykknd

GraalVM native-image

native-imageと言う形式のバイナリを作成することで即実行可能な形式にすることで、初回起動速度を上げれる
https://aws.amazon.com/jp/builders-flash/202110/jvm-lambda-function/?awsf.filter-name=*all
これを使えるFWを探す

Micronaut vs Spring Native vs quarkus

現行Springの資産はない(!)のでSpring Nativeを利用する理由が特にない
QuarkusはKubernatesの起動初速を上げるようなモチベーションで開発されていたようなので
Micronautを採用してみよう。何と言っても速いらしいし(安直)

Micronaut ハンズオン

https://www.youtube.com/watch?v=vYmk5-JynvQ

注意点は
①流れるように操作しているが、macで作成したnative-image(3:10)をAWS Lambdaで起動することはできない。なのでどこかのLinuxでビルドし直したものをアップロードしている(4:35)ようだ。
(コメント欄にも同様の指摘あり。ちなみにmacのnative-imageで起動すると exit 126とかになる)
②後に記載するがMicronaut3.3.1 と micronaut gradle plugin 3.2.0だとこの動画で実施しているようにカスタムしてmainクラスを使用する際の設定、mainClassを application から指定することができなかった。直接 graalVMのplugin側の引数として渡すことで起動する

以下は動画の流れを踏まえた上での手順補足

1. micronautプロジェクトをコピー

https://micronaut.io/launch/

必要なプロジェクトがDLとかgitHubにpushできる。こういうのほんとに助かる。
適当に選ぶと色々ついてくるが、DB周りのツールやDataDogなどはハンズオンでは避けた方が無難。
基本的には gradleに依存関係がついてくるようなイメージ。(micronaut-cli.ymlと言うファイルに記載されるようだ)
動画のとおりgraalVMのnative-image周りは必須。

2. GraalVMをDLしてPATHを通す

https://guides.micronaut.io/latest/micronaut-creating-first-graal-app-gradle-java.html

sdkmanでも直接DLしても。
直接の人はここからGitHubに。macOSってdarwinなんですね。

https://www.graalvm.org/downloads/

コードを書いてみるetc 略

3. native-imageをビルドしてあげてみる。

ポイントは3つ

  • Lambdaのruntimeをjava11ではなくprovided.al2(何とかなamazon linux2)にすること
  • よりよき人生とするためにnative-imageとbootstrapをzip化すること(3.05)
  • customのmainClass利用で、mainClassが指定できない(=mainClassが常にMicronautLambdaRuntime)場合はgraalvmPluginで以下の引数を渡す。

micronaut gradle pluginはこのgraalvmPluginを透過的に扱ってる(的な記事をどっかでみたような)のでこう言うことができたはず。

application {
    mainClass="com.mitsucaru.TenantInfoRuntime"  //だめでした
    mainClass.set("com.mitsucaru.TenantInfoRuntime") //これもだめ
}

graalvmNative {
    binaries {
        main{
            buildArgs.add('-H:Class=com.mitsucaru.TenantInfoRuntime')
        }
    }
}

これ何やってるかって言うと native-imageをビルドする際に引数としてどこをmainClassとするかを指定する(-H:Class=)んですが、application(micronaut gradle plugin)で指定してもうまくそれが伝達しなかったんですよね。ので直書きしてやりました、です。

(なんかmicronaut gradle pluginのソースみるにAWS Lanbdaで特定用件満たすとだと強制的に上書きって話もあるんですが、とにかくうまくいかなかったのでこれで回避してます。micronautさんの意図とずれてたら申し訳ないですが。)

4. テストしてみる

mainClassを自分で決めたい人用の余談。ビデオの通りなんだが、リクエストをAWSProxyRequestにしなくてはならないので面倒だったら、AbstractMicronautLambdaRuntimeを継承した自分のmainClassの引数の型前二つをhandlerと同じ引数の型にするとそのまま渡してくれる、よ。

これを

HandsOnRuntime extends AbstractMicronautLambdaRuntime<APIGatewayProxyRequestEvent,APIGatewayProxyResponseEvent,Request,Response> 

こう

HandsOnRuntime extends AbstractMicronautLambdaRuntime<Request,Response,Request,Response> 

と言う感じでまずはMicronautのハンズオンはこれでできるんじゃないかと。
繰り返しますが、AWSLambdaに上げるときのnative-imageのビルドはLinuxですよー

tkykkndtkykknd

Amplifyでnative-imageをpushする!

やっと表題まで来ましたね。。。

amplify add functionで作ったLambdaに先ほど作ったmicronautのプロジェクトをコピーしていきます。
注意点としてはbootstrapで別のプロジェクト名になってる人とかはその辺り確認お願いします。
bootstrapが間違ってるとLambdaで起動した時にexit 1とかでエラーになります。

気をつけることは3つだけです。
① amplify push をするのはLinux (また言ってるよこの人)
② gradle のバージョンを上げよう(7.3.1を利用しています)
 当然pathも旧バージョンではなくそっちに通しましょうね。
 ※この辺り私はすんなりいきましたが、問題が起きるかもしれません。
  余分な環境作るなどリスクを排してトライしてみてください。(Cloud9推し)
  開発環境大事ですし、あなたの時間はもっと大事です。
③ xxx-cloudformation-template.jsonのruntimeをprovided.al2にしよう

これを

 "Runtime": "java11",
        "Layers": [],
        "Timeout": 25

こう

 "Runtime": "provided.al2",
        "Layers": [],
        "Timeout": 25

さて、残る手順としてはgradle buildのタスクに nativeCompileを絡ませる、と言うことになりますがそれがこちらになります。該当functionのbuild.gradleのbuildZipあたりを以下のように変更

task buildZip(type: Zip) {
    mustRunAfter check          // testの前とかにこの時間かかるビルドしたくないので
    dependsOn nativeCompile     // buildZipやる前にnativeCompile
    from "./build/native/nativeCompile" // この階層にnaitve-imageができます
    from "./bootstrap"                             // bootstrapをzipに混ぜ込んじゃいます
    archiveFileName = 'latest_build.zip' // 初めからこの名前だからこの名前が良さそう
}
build.dependsOn buildZip  // おそらく元からある?

amplify push で nativeイメージがDeployされますね

今後の課題

  • macで amplify push できないし、普通の開発でnative-imageなんかいらないよね?
    これはif分を駆使して対応しようかなと思っています。
     開発環境では、Lambdaのruntimeをjava11で、buildZipの時にall.jar
     評価環境その他では Lambdaのruntimeをprovided.al2でnative-image

  • Amplify Consoleからのビルドができないじゃん
     自分のためにどこかでECRのコンテナをメンテしようと思います。

@tweet

このスクラップは2022/02/17にクローズされました