AWS Lambda と API Gatewayの連携でつまづいた

AWSを触り始めて間もない超初心者がLambdaでとあるサイトからスクレイピングして特定のデータを返すAPIを作成し、jQueryからCORSで呼び出そうとしたときに引っかかったことを備忘録として記します。

この記事はAWSを使い始めて2,3日の超初心者が書いているため間違った情報や、ベストな方法ではない内容が含まれている可能性があります。

使用AWSサービス

Lambda

PythonやNode.jsなどいろいろな言語の関数を実行できるサーバーレス環境。
今回はPythonを利用して動画サイトにスクレイピングをかけ、動画URLが記されたメタファイルのURLを取得してそれを結果として返すために使用

API Gateway

APIエンドポイントを作成でき、AWS内の様々なサービスとの橋渡しをしてくれる。
今回はLambdaと統合し、インターネットからLambda関数を呼び出すために作成。

つまづきポイント

VPCに接続するとLambdaからインターネットに接続できない

最初、Lambda関数を作成する時にVPCに接続する設定で作成してしまい、Lambdaからurllibでopenとかしてもタイムアウトになってしまった。

調べるとVPCに接続した場合はインターネットに出るのにNAT Gatewayが必要になることが分かった。しかしNAT Gatewayを使うにはEIPが必要で、それにはお金がかかる。ほかに方法はないか調べたらそのそもVPCに接続しなければNATも必要ないということが分かった。

  • Lambdaを使うにはVPCへの接続が必要だとの思い込み
  • Lambda単体で使うならVPCへの接続は不要

テストは成功するのにAPI Gatewayから呼び出すとInternalSeverError

Lambdaのテストでは成功するのに、APIを外から呼び出すとInternalSeverErrorとなる。CloudWatchのログを見てもエラーは出ていないのになぜエラー?どこでエラー?

エラーログが出ないのは、エラーが起きていた場所が、Lambdaではなく、API Gatewayだから。Lambda関数の結果をAPI Gatewayに渡す時形式が合っていなかったためエラーが起きていた。
そしてLambda関数自体は正常に終了しているためLambdaのエラーログには出てこない。

AWSのドキュメント「HTTP API Lambda 統合に関する問題のトラブルシューティング」を参考にHTTP API のログを見ることで、返り値が正しくなかったということを発見。

return {
         'statusCode': 200,
         'body': dict
     }
  • dictをjson.dumps()してstringifyせず、dictのままJSONに入れて返す。Lambdaのテストではエラーにならないが、API Gatewayを通るときにエラーになる。(JSONの中のデータをいちいち文字列化はしてくれない)

dict単体で返すのはOK

return dict

ステータスコード付きで返したい場合

return {
         'statusCode': 200,
         'body': json.dumps(dict)
     }

json.dumps()しなくてもdictのみを返す場合はエラーにならない。API Gatewayeがシリアライズしてくれる。

CORSエラー

CORSエラーを解決する前に、下記の違いを理解していなかったので解決方法をググってもそのページが言っていることが分からなかった。

  • HTTP APIとREST APIの違い
  • Lambda プロキシ統合と非プロキシ統合の違い

HTTPかRESTかは、タイプが「HTTP」だったのでHTTPだろうと推測、統合メニューをみるとLambda関数が表示されているので、おそらく統合されているのだろうと判断できた。

API Gatewayで CORS ヘッダーを返す

API GatewayでCORSを設定すれば、Lambdaで追加ヘッダーなどを設定する必要はないと理解

しかし、ブラウザでのCORSエラーが表示される。ネットワークリクエストを見るとOPTIONSメソッドがまず呼び出され、その後POSTが呼び出されているが両方とも失敗している。

OPTIONSメソッドはpreflightリクエストといって、実際のリクエストを送信する前にサーバーに確認をとるためのもの。preflightはいつも発生するわけではなく、一定の条件を満たさない場合に発生するらしい。今回はJSONをそのままPOSTするために、content-typeを指定していたのでこの条件に当てはまっていた。

preflightリクエストでもOPTIONSメソッドだろうが何だろうがAPI GatewayでCORSヘッダーを返す設定にしていればaccess-control-allow-originなどのCORSヘッダーはAPI Gatewayから返されるのでLambdaで何かしなくてもいいのだけどここに落とし穴があった。

Lambda関数で必要なクエリ文字列が正しくない時はステータスコード400を返すようにしていた。
→ OPTIONSメソッドの場合はクエリ文字列は渡されない
→ OPTIONSメソッドのリクエストにもエラーコード400が返される
→ その場合、access-control-allow-originヘッダーが含まれていても、ブラウザはCORSが許可されていないとみなして、リクエストが失敗する。

  • ルートをANYにしているのに、OPTIONSメソッドへの対応をLambda関数内でしない
  • preflightリクエストが発生する条件に合致していた
  • preflightリクエストが発生しないよう「シンプルなリクエスト」にする
  • preflightリクエストが発生するなら、OPTIONSメソッドがLambda関数にルーティングされているか確認
  • ルーティングされているならLambda関数内で適切にOPTIONSメソッドに対応する(OPTIONSメソッドはクエリ文字列が渡されないので、メソッドを判定せずエラーコードを返している場合はメソッドごとに分けるなどする)

API GatewayのCORS対応については下記のページが参考になった。

Comments

タイトルとURLをコピーしました