Lambda からのレスポンスサイズにはいろいろと制限があり、例えば同期呼び出しにおいて、関数 URL や API Gateway 経由だと 6MB、ALB 経由だと 1MB までといった制限があります。それ以上のデータを返す場合は、Lambda ではなく ECS に変えたり、S3 に出力してからごにょごにょする必要がありました。が、2023年4月に Lambda でストリームレスポンスがサポートされ、この制限を超えることができるようになりました。また、CloudFront の OAC (Origin Access Control) が Lambda の関数 URL をサポートしましたので合わせて検証してみました。
(検証で用いた CDK、Lambda のコードは GitHub に上げてあります。)
Lambda のストリームレスポンス
Lambda からの通常のレスポンスは一括で送られてきますが、ストリームレスポンスの場合は名前の通り、連続的にレスポンスを返すことができます。言い換えるとレスポンスに返すデータが全て揃ってから送る必要はなく、送れるデータから応答を開始することができるので、「最初の1バイトを受信するまでの時間」、いわゆる TTFB (Time To First Byte) を小さくすることができます。アプリケーションによっては UX の改善に繋げられ、例えば生成 AI を使ったチャットの応答なんかが一例になります。
このストリームレスポンスを使った時のペイロードサイズの上限は 20MB のソフトリミットとなっており、この値はリクエストすることで増やすことができるとされています。今回はこの機能を使って従来の一括レスポンス時の上限である 6MB を超えるデータを Lambda から返したいと思います。
また、ストリームで扱うことのもう一つの利点として、入出力のデータをうまく扱うことで、Lambda のメモリに大きなデータを全て載せる必要がなくなるため、Lambda のメモリサイズをより小さくできるというのがあります。
そしてこのストリームレスポンスで応答を得るには Lambda を “関数 URL” (Function URLs) や InvokeWithResponseStream
API などを使って実行します。
その他、ストリームレスポンスの詳細はドキュメントを見ていただければと思います。なお、ストリームレスポンスではリクエストごとに 6MB 以上のレスポンスを返す場合は別途料金がかかるのでよくご確認を (Lambda 料金ページ)。
Lambda の “関数 URL” (Function URLs)
関数 URL は Lambda に設定できる HTTP エンドポイントで、API Gateway などを使わずに直接 HTTP を介して Lambda 関数を実行することができます。詳細はドキュメントを。
気軽に利用できる関数 URL ですが、認証方式としては「認証なし (NONE
)」か「IAM 認証 (AWS_IAM
)」のどちらかとなっており、それ以外の手段でリクエストを検証したい場合は Lambda 関数の中で検証するコードを実装する必要がありました。
そのため、認証なしにするわけにもいかないし、IAM 認証はちょっと使いにくいし、、、と使い所が難しかったのですが、この度 CloudFront の OAC が関数 URL をサポートしました。
CloudFront が 関数 URL のための OAC をサポート
CloudFront が関数 URL のための OAC をサポートするアップデートが日本時間の 2024年4月12日にありました。これは関数 URL の認証方式として IAM 認証を使い、特定の CloudFront ディストリビューションからのみ関数 URL を実行できるように制限することで、CloudFront で利用できるアクセス制限が適用しやすくなりました (例えば AWS WAF)。
このアップデート以前でも関数 URL を CloudFront のオリジンに設定したり、API Gateway の HTTP プロキシ統合先に設定することはできましたが、関数 URL の認証を “認証なし” にする必要があり、これらをバイパスして直接、関数 URL にアクセスできてしまう経路が残るため Lambda 側で制限ロジックを実装したりする必要がありました。
検証したアーキテクチャ
というわけで今回は以下のようなアーキテクチャで検証してみました。S3 にある 6MB 以上のオブジェクトをストリームレスポンスを使って参照するという構成です。S3 にあるファイルであれば CloudFront から直接オリジンとして参照すれば良いだけなのであまり実用性ないですが、簡単にするためということで。
Lambda 関数のコード
Lambda 関数のコードは以下のようにしました。リクエストされたパスをそのまま S3 のオブジェクトキーとして S3 にアクセスし、オブジェクトのデータをストリーミングで返す関数です。
※ サンプルでは S3 のオブジェクトキーが分かればそのまま取得できてしまいますので、公開範囲、扱うデータに注意してください
|
|
ポイントとしては、、、
- 12行目 : ハンドラになる関数を
awslambda.streamifyResponse()
という謎のデコレータでラップしてあげます - 13行目 : デコレータは以下のパラメータを持つ関数を受け付けます
event
: いつもの Lambda を呼び出す時に渡されるイベントです。今回は関数 URL を使うので型的にはLambdaFunctionURLEvent
になりますresponseStream
: ドキュメントによると Node.js の WritableStream とのことですcontext
: いつもの実行環境の情報などを提供する Context オブジェクトです
- 30行目 : バイナリデータなどは Content-Type が
application/octet-stream
になるので、S3 から取得した時の Content-Type を設定します - 31行目 : レスポンスは
responseStream
にwrite()
することでもできるようですが、pipeline()
の利用が推奨されているようなのでお作法に則ります (catch
節の中ではwrite
を使っています)。- 本題から逸れますが
pipeline()
の第1 引数には、stream.Readable
な入力を指定するのですが、S3 からの GetObject のレスポンスのBody
プロパティがそのまま使えるようです。
- 本題から逸れますが
留意点としてはストリームレスポンスが利用できる AWS マネージドなランタイムは今のところ Node.js (14以降) のみとなっています。他の言語を利用したい場合はカスタムランタイムかLambda Web Adapter を利用します。
CDK コード
今回検証したものは CDK でデプロイしました。コードのほとんどはクラスメソッドさんの記事を参考にさせていただきました🙏
ストリームレスポンス関連やその他、変更したところを紹介します。
関数 URL 設定部分
関数 URL の設定には Lambda Function の addFunctionUrl
を利用します。そこで invokeMode
として RESPONSE_STREAM
を設定します。
const lambdaFunctionUrl = lambdaFunction.addFunctionUrl({
authType: cdk.aws_lambda.FunctionUrlAuthType.AWS_IAM,
invokeMode: cdk.aws_lambda.InvokeMode.RESPONSE_STREAM,
});
CloudFront ディストリビューションの構成
検証のためシンプルな設定にしました。Lambda の動作確認をしたかったのでキャッシュも無効にしています。
const distribution = new cdk.aws_cloudfront.Distribution(this, 'Distribution', {
defaultBehavior: {
origin: new cdk.aws_cloudfront_origins.FunctionUrlOrigin(lambdaFunctionUrl),
viewerProtocolPolicy: cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
cachePolicy: cdk.aws_cloudfront.CachePolicy.CACHING_DISABLED,
responseHeadersPolicy: cdk.aws_cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS,
},
});
※ 他に OAC 周りの設定などが必要です。クラスメソッドさんの記事か今回のコードを GitHub に上げてありますのでそちらを参照ください。
※ サンプルではドメイン名、S3 のオブジェクトキーが分かればそのまま取得できてしまいますので、公開範囲、扱うデータに注意してください
動作確認
準備
S3 バケットに 6MB を超えるファイルをアップロードしておきます。
aws s3 cp large.pdf s3://${BUCKET_NAME}/
適当なファイルが見つからない場合は以下の記事を参照ください。
curl で取得してみる
curl -v https://${CLOUDFRONT_DOMAIN_NAME}/large.pdf -o large.pdf
出力は一部省略しますが、ストリーミングでダウンロードできてそうです。
* (省略)
> GET /large.pdf HTTP/2
> Host: dXXXXXXXXXXXXX.cloudfront.net
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/2 200
< content-type: application/pdf
< (省略)
{ [15922 bytes data]
念のため、HTTP1.1 でも確認してみました。CDK でデフォルトでディストリビューションを作成すると HTTP2 になるので、リクエスト時に --http1.1
を指定します。レスポンスヘッダに Transfer-Encoding: chunked
が出てこれば良さそうです。
curl -v --http1.1 https://${CLOUDFRONT_DOMAIN_NAME}/large.pdf -o large-http1.1.pdf
出力結果
> (省略)
> GET /large.pdf HTTP/1.1
> Host: dXXXXXXXXXXXXX.cloudfront.net
> User-Agent: curl/8.4.0
> Accept: */*
>
< Content-Type: application/pdf
< Transfer-Encoding: chunked
< Connection: keep-alive
< (省略)
0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0{ [16384 bytes data]
100 6858k 0 6858k 0 0 1352k 0 --:--:-- 0:00:05 --:--:-- 1792k
chunked
になってました。良さそうです。
まとめ
Lambda のストリームレスポンスを使って、6MB 以上のファイルを “関数 URL” を使って取得してみました。今回は CloudFront の OAC が関数 URL をサポートしたこともあり、併せて使ってみました。WAF などと組み合わせて利用しやすくなったので、関数 URL の利用シーンも増えるのではないかと思います。
今回検証したような静的なファイルをそのまま Lambda からストリーミングで返すことはユースケースとしてはあまりなさそうです。Lambda で動的に大きなファイルを生成する場合や、大きなファイルをベースに Lambda で加工してから返す (画像に透かしを入れるとか) といったケースなんかが使い所でしょうか。
最後に・・・
この投稿は個人的なものであり、所属組織を代表するものではありません。ご了承ください。