AWS Lambda Function URLsをSPAから直に叩く

AWS LambdaにはFunction URLsという機能がある。提供されたのはわりと最近 (2022年4月) のことだ。

AWS Lambda Function URLs の提供開始: 単一機能のマイクロサービス向けの組み込み HTTPS エンドポイント | Amazon Web Services ブログ

Function URLsを使うとAPI Gatewayを噛ますことなくLambda関数にHTTPSエンドポイントを用意することができる。API Gatewayが有料なのに対してFunction URLsは無料だ!いやLambdaの使用分の課金があるが、リクエスト毎月100万件まで無期限無料なので問題なかろう。

そういうわけで、Github Pages上のSPAからFunction URLsを直に叩く構成なら無期限無料で簡単なWebアプリをデプロイできそうだと思い立った私は、実際なんか作ってデプロイしてみた……

どうだったのか

作ったのは brainf*ckを吐く自作言語のPlayground だ。自作言語のコンパイラ (OCamlで書いた) の実行ファイルを載せたコンテナイメージ利用のLambda Functionで 自作言語 → brainf*ck の変換をして結果を返す。

で、まあ問題なく動いたのだが、いくつか微妙な点があってデプロイ後にAPI Gatewayを使う構成に変えてしまった。「微妙な点」とは具体的に次の2つだ:

  • ローカルでの動作確認が難しい。
  • ロットリング機能がない。
    • AWS Lambdaはかなりスケールするやつなので、DoS攻撃とかで大量のリクエストが来たとき全部捌いてしまいその分の料金を支払うことになる。
    • リクエスト100万件につき0.2ドルなので大した金額にはならないし、そもそも攻撃するメリットが無いのであまり気にせんで良いのかもしれない。
    • 例えば私が誰かの恨みを買ったとして、そいつが私のURLに向けてリクエストを1億件 (←秒間1000件でも24時間かかるな) 送りつけて、そのとき仮に円安がヤバくて1ドル=360円とすると、私はAWS7200円くらい支払うことになる。……微妙だ。

さきほどAPI Gatewayが有料と述べたが、1ドル=360円のレートでも課金は2800件につき1円程度だ。月1000件もリクエストは来ないと思うし、100円くらいなら払っても構わないし、冷静に考えてみるとこの用途では料金の観点でもFunction URLsを使う必要はなさそうだった。

ついでに言うと、コンテナ利用のLambda Functionを利用するにはECRにイメージをpushする必要があり、プライベートリポジトリはデータ量で課金される。私の場合 (12ヶ月の無料期間のあとに) 月額1円くらい取られるかも。無期限無料ではなかったな。

以下、Function URLsのデプロイまでの流れとかハマった点とかを記しておく。

記録

AWS SAM CLIというのを使うとサーバーレスアプリケーションのプロジェクト作成・ビルド・ローカル動作確認・デプロイが全部できるらしい。

チュートリアル: Hello World アプリケーションのデプロイ - AWS Serverless Application Model ←このページを参考にプロジェクトを作成する。

API Gatewayを消してFunction URLsを使う

template.yamlapp.jsを書きかえればFunction URLsでAPIをデプロイできるわけだな。

Hello Worldテンプレートのtemplate.yamlには最初からAPI Gatewayを使用する旨の設定が書いてあるわけだが、これを消す。

      Events:
        HelloWorld:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /hello
            Method: get

↑消すやつ。これの他にOutputs:以下にもAPI Gatewayに関する項目があって、そいつも消す。

さてAPI GatewayのかわりにFunction URLsの設定を書く。さっき消したEvents:と同じ場所にFunctionUrlConfigを書けば良いらしい。今回必要なのはAuthTypeCorsだ。

      FunctionUrlConfig:
        AuthType: NONE
        Cors:
          AllowOrigins:
            - http://localhost:3000
            - https://roodni.github.io
          AllowHeaders:
            - Content-Type

ブラウザから呼べねえ

よし動作確認するぞ!ローカルにエンドポイントを生やすコマンドはsam local start-lambdasam local start-apiの二通りがあるんだが、start-lambdaは (たぶん) ブラウザから叩くのに適さない。ここで使うのはstart-apiの方だ。

$ sam local start-api -p 3001
Error: Template does not have any APIs connected to Lambda functions

あれ?もしかしてFunction URLsってSAM CLIで動作確認できない??いや設定書けるのにテストできないってのは変だろ。やり方を調べるぞ……

API Gatewayで動作確認する

もういい。Lambda 関数 URL の呼び出し - AWS Lambda ←このページによればリクエストイベントの形式はFunction URLsとAPI Gatewayでだいたい同じらしいので、ローカルで試すときはAPI Gatewayを使ってやる。

CORSの設定のためResources:以下に AWS::Serverless::HttpApi - AWS Serverless Application Model を書く。

  BfreCompileApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      CorsConfiguration:
        AllowOrigins:
          - http://localhost:3000
        AllowHeaders:
          - Content-Type

BfreCompileApiというのは私が適当につけた名前なので気にしないでほしい。

さっき消したEvents:をいろいろ書き換えて復活させる。

      Events:
        BfreCompileEvent:
          Type: HttpApi
          Properties:
            ApiId: !Ref BfreCompileApi
            Path: /codegen
            Method: POST

TypeApiからHttpApiに変えたほかApiIdという項目を追加した。

これでプリフライトリクエストが通るようになる。さらにLambda Functionのハンドラをいじって返値のheadersプロパティにAccess-Control-Allow-Originを付与してやることで、ようやくsam local start-apiのエンドポイントをブラウザから叩けるようになる。

デプロイ用に修正が要る

sam deploy --guidedでアプリケーションをデプロイできるが、API Gatewayはあくまで動作確認用なのでデプロイ前に関連する設定をyamlからコメントアウトしておく。←アホ

デプロイしたらCORSエラーが出た。ヘッダが重複……?うわっハンドラで手動付与したCORSヘッダとFunction URLsの設定のCORSヘッダが二重になって返されてやがる!! もう知らん。デプロイ前にCORSヘッダの手動付与する部分をコメントアウトして対処する。←アホ

ちなみにAPI Gatewayでデプロイする場合、手動付与のCORSヘッダが存在してもAPI Gatewayの設定を優先して塗り潰してくれるので問題がないのだが、ならsam local start-apiでもそうしてくれたら最初から手動付与しなくても良くなって万事解決だよな。挙動が謎だ。

最後にリクエストボディがBase64エンコードされる場合があることに引っ掛かったが、これはイベントオブジェクトのisBase64Encodedを見れば対応できるので大した問題ではない。

動いた

わぁい