AWS Lambda の 250MB 制限をなんとか回避した話 (Java 編)

エンジニアの村松です。ゴールデンウィークが終わりしばらく経ちますが、みなさん五月病とかは大丈夫でしょうか。年齢を重ねるにつれて時が過ぎるのは早く感じるものです。私はまだ2月くらいの気分です。

さて、今回は AWS Lambda の 250MB 制限をなんとか回避した話を紹介します。

背景

アソビューでは、メール配信などの通知システムに AWS Lambda を利用しており、Lambda 関数は Java + Spring アプリケーションと Serverless Framework を利用して実装しています。

いつものように何気なく開発をしていたところ、serverless deploy がエラーを吐いており、CircleCI が失敗していました。

An error occurred: Resource handler returned message: "Unzipped size must be smaller than 262144000 bytes (Service: Lambda, Status Code: 400, Request ID: ...)"

このエラーメッセージを見たらだいたい内容お察しが付くと思います。が、どうか最後まで読んでいただければと。なんなら最後の採用ページだけでも🙆‍♂️

Lambda 関数では、アップロードしたデプロイパッケージの解凍後のサイズは 250MB 未満である必要があります。これまで、アプリケーションを Gradle の shadowJar タスクでビルドし、生成した jar ファイルをそのままアップロードしていました。jar ファイル自体は 90MB ほどだったものの、解凍後のサイズが 250MB を超えてしまっている状況でした。

試してみたこと1

エラーメッセージでググってみると過去の事例がたくさん出てきます。先人の皆様に感謝しつつ最終的に コンテナイメージ に辿り着きました。最大 10GB までイケるとのことでもうこれでいいじゃんとなりました。ここでもう内容お察しが付くかも知れませんが、どうか最後まで読んでいただければと。

docs.aws.amazon.com

コンテナ移行

ベースイメージは public.ecr.aws/lambda/java:11 を利用します。Gradle の shadowJar でビルドするところはそのままにしつつ、jar を含めてイメージを生成して ECR にプッシュします。

FROM public.ecr.aws/lambda/java:11

COPY build/libs/application.jar ${LAMBDA_TASK_ROOT}
RUN ls -al
# jar そのままでは関数実行できないようなので jar コマンドで展開する
RUN jar -xvf application.jar

serverless.yml ではコンテナイメージのタグを指定します。

functions:
  <function_name>:
    image:
      uri: <account_id>.dkr.ecr.<region>.amazonaws.com/<repository>:<tag>
      command:
        - <handler>
... (略)

serverless deploy を実行して Lambda 関数が正常にデプロイされることを確認し、メール配信をテストしてみたところメールが来ず...。今回、ゼロから環境を構築したわけではなく既存の環境をなんとか移行しようとしたところでうまくいかずハマってしまいました。デバッグしてみたもののなかなか原因特定できず、別の方法を探すことにしました。

試してみたこと2

AWS Lambda のベストプラクティスに次のような説明があります。

docs.aws.amazon.com

Java で記述された デプロイパッケージを Lambda で解凍する所要時間を短縮します。そのために、依存する .jar ファイルを別個の /lib ディレクトリにファイルします。これで関数のすべてのコードを多数の .class ファイルと一緒に単一の Jar に収納するよりも高速化されます。手順については、「.zip または JAR ファイルアーカイブで Java Lambda 関数をデプロイする」を参照してください。

これまで、ビルドした jar ファイルをそのままアップロードしていたのですが、Lambda 関数をデプロイする際に jar が解凍されて大量の class ファイルがデプロイされているようでした。これを lib フォルダに jar をコピーし、zip ファイルでアップロードするようにします。

task buildZip(type: Zip) {
    into('lib') {
        from shadowJar
    }
}
assemble.dependsOn buildZip

build/distributions 配下に zip ファイルが生成されるので serverless.yml ではこの zip ファイルを指定します。

package:
-  artifact: build/libs/application.jar
+  artifact: build/distributions/application.zip

serverless deploy すると、以前は jar を解凍して class ファイルが展開されていましたが、zip を解凍して jar ファイルが展開される形になりました。Lambda 関数上はこのようにデプロイされていると思われます。

# ----- Before -----
${LAMBDA_TASK_ROOT}
 ┣ ...
     ┗ ...
     ┗ ...
 ┣ ...
     ┗ ...
 ┣ ...
     ┗ ...
 ┗ ... (依存ライブラリ含めた大量の class ファイルが展開される)

      ↓↓

# ----- After -----
${LAMBDA_TASK_ROOT}
 ┗ lib
     ┗ application.jar

jar ファイル自体は 90MB ほどなので、これで AWS Lambda のサイズ上限を回避することができました。

今ではコンテナイメージを利用する方法が一般的ではあると思いますが、同じような状況に陥っている方には今回の方法はわりと簡単にできるためおすすめです。ご参考にしていただければと。

まとめ

  • アップロードしたパッケージは Lambda 関数をデプロイする際に解凍される
  • Lambda 関数では jar 形式のままで関数が実行できる
  • jar ファイルを zip にパッケージングしてアップロードする
  • ベストプラクティスはちゃんと読む

アソビューでは「生きるに、遊びを。」をミッションに、一緒に働くメンバーを募集しています!ご興味がありましたらお気軽にご応募いただければと思います!

www.asoview.com