ローカル環境で使ってたREST APIをLambdaにデプロイしようとしたらハマった件

はじめに

技術革新は絶え間なく進むもので、その波に乗るためには、新しいアイディアを迅速に形にして検証することが不可欠です。ネットオンでも、経営層から提案された新しい技術的アイディアの実現可能性を探るため、PoC(Proof Of Concept:概念実証)を実施することになりました。PoCの目的は大きくは以下の2点です。

  • アイディアが技術的に実現可能かどうかを検証する。
  • アイディアを実現したアプリを経営層自身に直接触れてもらい、その価値と実用性を確認してもらう。

 

この目的を果たすために、効率的かつ迅速にデモアプリ(プロトタイプ)を構築することが求められました。重要なのは、技術検証のコアとなる部分の開発に注力し、それ以外の部分に費やす時間を可能な限り少なくすることです。

そこで選んだのが、REST APIを用いたシンプルなバックエンドの実装です。開発初期段階では、要件の追加・変更や軌道修正がつきものなので、素早く修正・再デプロイできるローカル環境での開発が中心になりました。

しかし、デモアプリがほぼ完成し、経営層に使ってもらうためにAWS(API Gateway + Lambda)へのデプロイを試みたところ、想定外の問題に直面しました。このブログ記事では、REST APIをAWSへデプロイしようとしたときに直面した問題と、それを解決するために行った対策について記述します。

技術スタック

開発初期段階では 素早く修正・再デプロイできるローカル環境で開発し、経営層に使ってもらうときにAWSへデプロイする想定です。フロントエンドはそのまま使い回せるように、技術スタックの選定にあたっては「AWSへのデプロイ前後で、REST APIエンドポイントのURLやインターフェースが変わらない」という条件を満たす必要があります。

デプロイで使用するAWSサービス

以下の通り、コスト効率や管理の手軽さを理由に「API GatewayとLambda」を使うことにしました。

  • コスト効率:サーバーを24時間稼働させるよりもコストが安く済む
    • デモアプリの利用頻度は高くない。
    • Lambdaは従量課金(リクエスト数と実行時間に基づく)かつ無料枠がある。
  • 管理の手軽さ:
    • デモアプリでは 技術検証のコア部分以外には時間や労力を割きたくない。
    • API Gateway + Lambdaはサーバーの設定・管理・保守が不要。

言語・フレームワーク

REST API構築にあたっては、PoCのテーマによって「TypeScript(Express)」「Python(FastAPI)」を使い分けました。基本的には、業務で使い慣れている TypeScript(Express)を使い、機械学習やデータサイエンス系の実装が必要になる場合はPython(FastAPI)を使いました。

いずれの言語・フレームワークの場合も、特定のライブラリを使うことで エンドポイントのURLやインターフェースを維持したまま API Gateway + Lambda環境に持ち込むことができます。

  • TypeScript(Express)
    • aws-serverless-expressserverless-httpなどのライブラリを使うことで、LambdaとAPI GatewayのリクエストをExpressのリクエストオブジェクトに変換し、レスポンスをAPI Gatewayが理解できる形式に変換できる。
  • Python(FastAPI)
    • mangumというライブラリを使うことで、リクエストとレスポンスをLambdaとAPI Gatewayが扱える形式に変換できる。
  • API Gatewayの設定
    • API Gatewayでプロキシ統合を設定すると、すべてのリクエストをLambdaへ直接転送し、Lambdaからのレスポンスをクライアントに返すことができる。これにより、ローカルでのAPIのルーティング設定をそのままAWS上で利用することが可能となる。

これらの技術スタックを選定した時点で、ローカル環境で動作確認した REST API を、そのままAWS環境で使うのは簡単だと考えていました。

AWSへのデプロイ時に直面した問題と解決策

ローカル環境で動作確認したREST APIをAWSにデプロイする過程で、いくつかの技術的な問題に直面しました。これらの問題は、ローカル環境では明らかにならなかったもので、API Gateway + Lambdaでの実行を前提とするときに初めて顕在化するものでした。ここでは、3つの問題と私が講じた対策について述べます。

問題1: レスポンスタイムの制限

私が実装したAPIの中には、レスポンスに30秒以上かかる処理が含まれていました。これはAPI Gatewayの利用シナリオとしては不適切でした。API Gatewayのクォータは「統合のタイムアウト:Lambda、Lambda プロキシ、HTTP、HTTP プロキシ、AWS 統合など、すべての統合タイプで 50 ミリ秒〜29 秒」と規定されており(2024-04-12現在)、それを超過したことが原因です。

Lambdaの実行時間の上限が15分間であることは知っていましたが、API Gatewayのタイムアウト制限は盲点でしたので、個人的には勉強になりました。

解決策

API Gateway + Lambdaの構成で REST APIとして実装する場合、レスポンスを29秒以内に返す必要があります。そこで、Lambdaを「REST API用」と「非同期処理用」に分割しました。

  • REST API用 Lambda
    • エンドポイントを「ジョブ受付用」「ステータス確認用」の2つを設ける。
      • ジョブ受付用:非同期処理用Lambdaを開始し、ジョブのidをすぐにレスポンスする。
      • ステータス確認用:ジョブのidをパラメータとして受け取り、当該ジョブのステータスをレスポンスする。(ステータスはDynamoDB等にもたせる)
  • 非同期処理用 Lambda
    • ジョブのステータスを更新しながら 処理を実行する。

この対策により、レスポンスタイムの制限を回避することができました。上記以外にもさまざまな方法が考えられると思いますが(例えばポーリング方式ではなく SNSを使った通知方式にする、 Step Functionsを使うなど)、今回は「元のコードをそのまま使うこと」に重きを置きました。

問題2: レスポンスサイズの制限

私が実装したAPIの中には、レスポンスサイズが10MBを超えるものが含まれていました。データ分析の結果(全件!)をJSONで返すエンドポイントです。Lambdaのクォータは「呼び出しペイロード (リクエストとレスポンス):リクエストとレスポンスにそれぞれ 6 MB (同期)」と規定されており(2024-04-12現在)、それを超過したことが原因でした。

解決策

データの分析結果を返すエンドポイントを以下のように修正しました。

  • データ分析結果を全件レスポンスするのではなく、LIMIT/OFFSET方式で絞るようにした。
  • エンドポイントを「一覧取得」「詳細取得」の2つに分割した。
    • 分析結果に長いテキストが含まれている場合、「一覧取得」では一定の長さでトリミングした値を返す。テキスト全体が必要な場合は 都度 「詳細取得」する。

こちらの問題については、Lambdaのクォータを把握できていなかったのももちろん反省すべきですが、ローカル環境への甘え(重いデータでも低レイテンシでレスポンスされる!)があったことが一番の反省点でした。

問題3: デプロイサイズの制限

問題2でも述べた通り、私が実装したAPIの中には データ分析結果を返すものがありました。データ分析結果の元データのサイズは合計すると 200MB程度になります。実際には このデータだけでなく 依存パッケージ等も含めてデプロイされますので、Lambdaのクォータである「デプロイパッケージ (.zip ファイルアーカイブ) のサイズ:50 MB (zip 圧縮済み、直接アップロード)、250 MB (解凍後)」(2024-04-12現在)を超過してデプロイに失敗することがありました。

解決策

もともとは1つの「API Gateway + Lambda」リソースに全エンドポイントをデプロイしていましたが、以下のようにエンドポイントを複数の「API Gateway + Lambda」リソースに分散させることで対応しました。

  • もともと:
    • リソース①: 全データ(200MB)
  • 分散後(イメージ):
    • カスタムレイヤー: もともとのコードは変更せずにレイヤーに含めておく
    • リソース①: エンドポイントAで使うデータ(60MB)、レイヤーのコードを呼び出す処理のみ記述
    • リソース②: エンドポイントBで使うデータ(70MB)、 〃
    • リソース③: エンドポイントCで使うデータ(70MB)、 〃

ほかにもコンテナイメージを使う方法でも解決できたと思いますが、コンテナイメージでデプロイした実績がなく、そこへの検証時間を割きたくなかったため、今回は上記の方法を選択しました。

まとめ

この記事では「ローカル環境で使ってたREST APIをLambdaにデプロイしようとしたらハマった件」と題して、AWSデプロイ時に直面した3つの問題と解決策について述べました。

AWSサービスのクォータについて知っていれば もちろん回避できましたが、たとえクォータについて知らなくても、アプリケーションとしての基本的な品質を保てていれば 問題1と2は防げていたはずです。

当初、ローカル環境で動作確認するようにしたのは「素早く修正・再デプロイできる」からであって、「レスポンスタイムやサイズを考慮しなくていい」からではなかったはずなので、甘えが出てしまったことへの反省を次の開発に活かしたいと思います。

参考文献