SpringCloudFunctionとAWS Lambda/Lambda Layers/ServerlessFrameworkを試す

この記事はaratana advent calendar 23日目の記事です。

# 動機
### Serverless(AWS Lambda)+Java
AWS Lambdaは、アクセスされる度実行されるため、初回起動時間が短いnode.jsやpythonが一般的かと思いますが、関数の数が増えてくる、結構な規模のアプリケーションになってくると今度は型がない言語であることが辛くなってきます。

起動が遅い問題については、[serverless-plugin-warmup](https://www.npmjs.com/package/serverless-plugin-warmup)などで解決可能なので、Javaなどの静的型付け言語は現実的な解となりうるのではないかと思いました。

Scalaなんかもいい選択肢ではありますが、やはり僕のなかでは[SpringFramework(SpringBoot)](https://spring.io/)の使い勝手とドメイン駆動設計に即したアーキテクチャ(＠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](https://docs.aws.amazon.com/lambda/latest/dg/configuration-layers.html)です。

プロダクトのコードとライブラリのコードを分けられる、さらに分割できるので、50MB制限を気にせず開発できます。

東京リージョンでも利用可能で、Serverless Frameworkも既に[対応済み](https://serverless.com/framework/docs/providers/aws/guide/layers/)です。

### 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のレポジトリ](https://github.com/spring-cloud/spring-cloud-function)を比較しながら足りないものを補う必要がありました。

### Lambda Layers
Lambda Layers用のサブプロジェクトを作成しています。([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/tree/master/libraries))

Lamdba Layersにはjarライブラリ群をラップしたzipを上げないといけないので、gradleにzipAppタスクを追加しています。([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/blob/096f974384d15f06b4b1a4cfcbe2e8bfcc7d48a0/libraries/build.gradle#L89))

プロダクト用のサブプロジェクトからはこのライブラリ用のサブプロジェクトを依存関係に指定していますが([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/blob/096f974384d15f06b4b1a4cfcbe2e8bfcc7d48a0/goods/build.gradle#L78))、プロダクトのjarには含まないようにしています。

### Serverless FrameworkのLambda Layers設定
#### Layersの設定

ポイントはOutputsの設定。プロダクト用のプロジェクトとスタックを分けるためOutputを設定しています。`Ref: JavaLibrariesLambdaLayer`の箇所は命名規則があって、`layers/javaLibraries`の`javaLibraries`を**先頭大文字**、語尾に`LambdaLayer`という文字を付けるというややこしい制約があります。

このあたりドキュメントに記載はなく、ブログで紹介されているのみなのでそちらを参考に。(https://serverless.com/blog/publish-aws-lambda-layers-serverless-framework/) 

```yml
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を使用するための設定をします。

```yml
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を設定し([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/blob/096f974384d15f06b4b1a4cfcbe2e8bfcc7d48a0/goods/src/main/java/handlers/GoodsHandler.java#L7))、serverless.ymlで環境変数`FUNCTION_NAME `でそのComponentを指定するといけます([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/blob/096f974384d15f06b4b1a4cfcbe2e8bfcc7d48a0/goods/serverless.yml#L21))。

gradleではAWS用のjarを作成するのに、shadowプラグインをそのまま使えず、META-INFのファイルをjarに含めるための設定がいくつか必要でした([該当コード](https://github.com/YasuhiroKimesawa/spring_cloud_function_lambda_layers_sample/blob/096f974384d15f06b4b1a4cfcbe2e8bfcc7d48a0/goods/build.gradle#L51))

# まとめ

まだ、SpringCloudFunction自体広まってないですし、AWS LambdaでJava・SpringBootを使うなんてという方も多いと思いますが、実現可能になってきましたね！