📑

Serverless Framework で Lambda の zipファイル制限を超えるアプリケーションをデプロイするために検討したこと

2023/03/27に公開

AWS Lambda で Python ランタイムを利用していると、時折 zipファイルのサイズが大きくて、Lambda 関数がデプロイできない 問題に直面します。例えば、私が昔よくやっていた機械学習もその問題を抱えている領域で、例えば docker のイメージをどんなに頑張ってスリムに作っても数GBを超えてしまいます。

このzipファイルが大きい小さいについては、一概に良し悪しが言えるものではなく、どんなにサイズが大きくても必要な要件を適切に解決できるのであれば、そちらを優先して採用すべきでしょうし、「そこまでする必要ある?」と思われるなら別の選択肢を選ぶ方が適切でしょう。

本記事では、「要件を満たすにはどうしてもzipファイルのサイズが大きくなってしまうため、Lambda に zipファイルをアップロードできなくなってしまった」問題に対して、検討した打ち手と考察を紹介し、最終的にどのような意思決定を行なったかを共有します。Lambda でサーバーレス構成を採用しており、アプリケーションサイズが大きくなったり、MLなどで大きなライブラリを利用したくなった場合にご活用いただけますと幸いです。

最終的にDockerコンテナを使用して課題を解決した

とある機能を開発している過程で以下の2点の問題に直面しました。

  1. 使用している単一のライブラリのサイズがそもそも大きく、zipファイルのサイズが制限を超えてしまう
  2. C, C++ など、当該ライブラリを使用するにあたって、Python以外の依存関係が発生してしまう

限られたリソースで効率的に顧客に価値提供していく体制を整えるため、なるべくサーバーレス構成を採用していく方針をとっています。ゆえに、この二つの問題を弊社はなるべくサーバーレス構成のまま解決しました。(サーバーレス以外の選択肢があることは承知の上です)

最終的に弊社では、

  • サイズの大きなライブラリを使用する箇所が限定的であったこと
  • 既存のアプリケーションに影響が出ない方法を採用したかったこと

などを理由に、Dockerコンテナを使用する方法で、一部機能についてはサービスを提供しています。

選択可能な方法は3つあった

取りうる手段は調べた限り以下の方法があります。以降ではそれぞれの方法とメリット・デメリットをまとめていきます。なお、これらの方法 Python Requirement と docker の組み合わせ以外は併用可能です。

  1. Serverless Python Requirements のオプションを利用する
  2. CloudFormation のスタックごとに、ライブラリを個別管理する方法
  3. Docker コンテナを使用する

Serverless Python Requirements のオプションを利用

Python で Serverless Framework を利用する場合は必ず使うであろう、Serverless Python Requirements というプラグインに提供されている機能を駆使して、どうにかしてライブラリのサイズを縮小したり、切り出したりする方法です。非常にテクニックっぽい方法ですが、手間は少なく済むため、最初に検討してみる価値のある選択肢です。

https://www.serverless.com/plugins/serverless-python-requirements

方法はいくつかあるのですが、代表的なものを列挙すると

  1. slim: trueslimPatterns, noDeploy を設定して、デプロイ時に不要なファイルを削除する(詳細)
  2. 関数ごとに requirements.txt を定義し、デプロイ時に個別に zipファイルを作成する(詳細
  3. zip: true を設定して、大きなライブラリであっても、小さなzipファイルが生成されるようにデプロイを実施する(詳細

特に、3の方法は公式ドキュメントにも記載されている通り、numpy, scipy, scikit-learn など大きな依存関係を提供することを想定された機能です。zip:trueを選択する前は失敗したデプロイも、設定後はうまくいくことを確認しています。利用する際の注意点はいくつかあります。

  1. すべての関数の冒頭に所定のおまじないコードを書く
  2. Lambda の初期化時に、zipファイルを展開するため、コールドスタートの時間が増大する可能性を気にする場合は Provisioned Concurrency や EventBridge での定期実行など対策を施す

1 のおまじないコードは以下です。

try:
  import unzip_requirements
except ImportError:
  pass

仕組みとしては、デプロイされた zipファイルを調べた限り以下のことが行われているようです。zipファイルをアップロードしていることには変わりありません。

  1. Lambda にアップロードする zipファイルに、依存関係を zip化したファイルを含める(そうすることで、zip展開後のファイルサイズが縮小される)
  2. Lambda の起動時に、unzip_requirements.py を実行し、zip化していた依存関係を展開する
  3. zipを展開した後の Lambda 関数は設定していた依存関係を使用できる

また、2のコールドスタートについては以下のシリーズを読み込めば方針は検討しやすくなります。
https://aws.amazon.com/jp/blogs/news/operating-lambda-performance-optimization-part-1/

スタック分割

Serverless Framework で提供しているアプリケーションが少し複雑なものになってきており、FaaS特有の「関数が立ちすぎてて収集つかない」問題や、弊社のように単一の Lambda が運用されていることで、運用しているアプリケーションがモノリシックに"なりすぎている"と感じた場合は有効な手段です。その時の組織の状態や戦略などを踏まえ、モジュラーモノリスやマイクロサービスなどを検討している場合は有効な手になるでしょう。

CloudFormation の公式ドキュメントに記載されたベストプラクティス[2]では、以下のような説明がなされています。

AWS リソースのライフサイクルと所有権を使用して、各スタックでどのリソースを使うを判断します。最初はすべてのリソースを 1 つのスタックに置いてもかまいませんが、スタックの規模が大きくなり範囲が拡大するにつれて、単一のスタックの管理は面倒で時間かかる場合があります。共通のライフサイクルと所有権を持つリソースのグループ化により、所有者は独自のプロセスやスケジュールを使用して、他のリソースに影響を与えることなくリソースのセットを変更できます。

つまり、大きくなってきたスタックを分割することで、使用するライブラリの数が減少し、問題が解決する、というシナリオです。実施方法は以下の2パターンが考えられるでしょう。

  1. シンプルにスタックを分割して使用する
  2. ネストされたスタックを使用する

分割してしまうか、ひとまとめのまま階層わけするかの違いであり、サービスの特性やアプリケーションの今後の展望に応じて、適切なものが採用されるのが望ましいです。

https://www.serverless.com/plugins/serverless-plugin-nested-stacks

Dockerコンテナを利用

最後に、Dockerイメージを使用する方法です。この方法は、Python以外の実行環境に関する制約をコントロールしたり、今後もライブラリサイズが増加することが見込まれる場合は有効でしょう。また、Docker のベストプラクティス(ビルド時間の短縮やマルチステージビルドの活用)は、Lambda においても有用で、この知見をすでに持っているエンジニアは多いかもしれませんね。

やり方はシンプルです。

  1. AWS の公式に従い Lambda 用の Dockerfile を作成する([参考](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html))
  2. Serverless Framework の公式に従い、作成した Dockerfile でビルドされるイメージを ECR にプッシュする設定を入れる(参考
  3. Docker イメージを使用したい関数の定義を変更する

なお、このデプロイ方法で最も注意が必要な点は、すでにzipファイルで提供している関数を置き換えることはできない という点です。公式でも以下のように説明されています。

既存の .zip ファイルアーカイブ関数を、コンテナイメージを使用するように変換することはできません。この場合は、新しい関数を作成する必要があります。[4]

つまり、既存の関数を docker コンテナで提供しようとする場合は、以下のような手順を踏む必要があります。ゆえに、新規で関数を提供する場合は気楽に提供できますが、既存関数を改善する場合は注意が必要ですね。

  1. docker コンテナで定義した新しい関数をデプロイ
  2. 新しい関数を利用するようにコードを修正
  3. 利用がなくなったら、昔の関数を削除(任意)

まとめ

今回は、「要件を満たすにはどうしてもzipファイルのサイズが大きくなってしまうため、Lambda に zipファイルをアップロードできなくなってしまった」問題に対して、検討した打ち手と考察を紹介し、最終的にどのような意思決定を行なったかを共有しました。

脚注
  1. https://www.serverless.com/plugins/serverless-python-requirements#:~:text=Dealing with Lambda's size limitations ↩︎

  2. https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/best-practices.html#organizingstacks ↩︎

  3. https://www.serverless.com/plugins/serverless-offline ↩︎

  4. https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-images.html#:~:text=ができます。-,注記,-既存の .zip ↩︎

トドケールテックブログ

Discussion