この記事はaratana advent calendar 23日目の記事です。
動機
Serverless(AWS Lambda)+Java
AWS Lambdaは、アクセスされる度実行されるため、初回起動時間が短いnode.jsやpythonが一般的かと思いますが、関数の数が増えてくる、結構な規模のアプリケーションになってくると今度は型がない言語であることが辛くなってきます。
起動が遅い問題については、serverless-plugin-warmupなどで解決可能なので、Javaなどの静的型付け言語は現実的な解となりうるのではないかと思いました。
Scalaなんかもいい選択肢ではありますが、やはり僕のなかではSpringFramework(SpringBoot)の使い勝手とドメイン駆動設計に即したアーキテクチャ(@Service/@Repositoryのようなアノテーションなど)がわかりやすく、使いやすいと思っています。
Lambda Layers
しかし、JavaをLambdaで使用する場合の難点として、zipで50MB、展開したファイルが250MB制限というのがあります。(https://docs.aws.amazon.com/lambda/latest/dg/limits.html)
Javaで特にSpringCloudFunctionを使おうとすると50MB近く行くのでなかなか厳しい。
そこで今年re:inventで発表されたLambda Layersです。
プロダクトのコードとライブラリのコードを分けられる、さらに分割できるので、50MB制限を気にせず開発できます。
東京リージョンでも利用可能で、Serverless Frameworkも既に対応済みです。
Spring Cloud Function
ややマニアックなプロダクトですが、開発は活発です。関数ベースのSpringBootで、AWS LambdaやAzure、Apache OpenWhiskにも対応しています。
https://github.com/spring-cloud/spring-cloud-function
特徴としては、
- SpringBootのエコシステムがそのまま利用できる。
- 関数をRestAPIサーバー的な感じで実行できる。
- RestAPIサーバー的な感じで実行するときは、ライブラリを実行時に取得するthin jarが作れる
- AWS LambdaやAzure、Apache OpenWhiskのアダプターが使える
というところでしょうか。特にRestAPIとして実行できるので、わざわざ(そして使いづらい)Lambdaのローカル実行をからデバックしなくてもOKなところが魅力的です。
試す
ということで全体のsampleソースはこちら。
https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample
ポイントを上げておきます。
gradle
SpringCloudFunctionではthin jar実行時にライブラリを取得するのため、mavenが基本となっていますが、今回は以下の目的のためにgradleを使っています。
- LambdaLayerを使用するため、ライブラリのプロジェクトとプロダクトのプロジェクトを分けるので、そもそもthin jarがいらない。
- mavenではマルチプロジェクトがあくまでモジュールとしての扱い、かつ親子関係が厳密で、ライブラリのプロジェクトとプロダクトのプロジェクトを分けてビルドするのが難しかった(何か方法があるかもしれないですが、解決できずgradleを利用しました)
SpringCloudFunctionのバージョン
試したのは2.0.0.RC2。RC版なので、まだそのまま持ってきても足りない依存関係などが結構あったので、gradle dependencies
タスクとFrameworkのレポジトリを比較しながら足りないものを補う必要がありました。
Lambda Layers
Lambda Layers用のサブプロジェクトを作成しています。(該当コード)
Lamdba Layersにはjarライブラリ群をラップしたzipを上げないといけないので、gradleにzipAppタスクを追加しています。(該当コード)
プロダクト用のサブプロジェクトからはこのライブラリ用のサブプロジェクトを依存関係に指定していますが(該当コード)、プロダクトのjarには含まないようにしています。
Serverless FrameworkのLambda Layers設定
Layersの設定
ポイントはOutputsの設定。プロダクト用のプロジェクトとスタックを分けるためOutputを設定しています。Ref: JavaLibrariesLambdaLayer
の箇所は命名規則があって、layers/javaLibraries
のjavaLibraries
を先頭大文字、語尾にLambdaLayer
という文字を付けるというややこしい制約があります。
このあたりドキュメントに記載はなく、ブログで紹介されているのみなのでそちらを参考に。(https://serverless.com/blog/publish-aws-lambda-layers-serverless-framework/)
layers:
javaLibraries:
package:
artifact: build/distributions/libraries-1.0-SNAPSHOT.zip
name: dev-javalibraries
compatibleRuntimes:
- java8
allowedAccounts:
- '*'
resources:
Outputs:
JavaLibrariesLayerExport:
Value:
Ref: JavaLibrariesLambdaLayer
Export:
Name: JavaLibrariesLambdaLayer
Lambda(Layersを使う側)の設定
Layersを別スタックにしたので以下のように、Outputを使用するための設定をします。
functions:
hello:
handler: org.springframework.cloud.function.adapter.aws.SpringBootStreamHandler
layers:
- ${cf:sampleJavaLayer-dev.JavaLibrariesLayerExport}
environment:
FUNCTION_NAME: goods
MAIN_CLASS: Application
events:
- http:
path: goods
method: post
integration: lambda
request:
template:
application/json: '$input.json("$")'
SpringCloudFunction
こちらは、この方のブログを参考にさせていただきました。
https://qiita.com/YusukeHasegawa/items/b9599834554b6ddb016e
概ねこの通りなのですが、いくつか補足を
複数の関数を同じスタックで管理するときは、Handlerの関数にComponentを設定し(該当コード)、serverless.ymlで環境変数FUNCTION_NAME
でそのComponentを指定するといけます(該当コード)。
gradleではAWS用のjarを作成するのに、shadowプラグインをそのまま使えず、META-INFのファイルをjarに含めるための設定がいくつか必要でした(該当コード)
まとめ
まだ、SpringCloudFunction自体広まってないですし、AWS LambdaでJava・SpringBootを使うなんてという方も多いと思いますが、実現可能になってきましたね!