[アップデート]AWS Lambdaでストリーミングな応答が可能になりました

2023.04.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

初めに

昨日のアップデートでAWS Lambdaは実行結果の返却値を一括の応答ではなくストリーミングな徐々に応答するようなことが可能となりました。
いざ日本語に直そうとすると微妙に難しいタイトルで実際の公式の翻訳がどうなるか次第では少しタイトルを調整するかもしれません。

これまでの方式ではLambdaの機能としては処理完了まで返却値を返すことができず、そういった機能が必要な場合はWebSocket等別の手段をユーザ側で実装する必要がありました。

今回のアップデートではTransfer-Encoding: chunked形式による返却に対応しHTTP/1.1の仕様の範囲内で徐々に値を返却できるようになりました。

またこの方式は応答サイズの上限が従来の6MBではなく20MBまでの対応となるためより大きなレスポンスを返すことができるようです。

Configuring a Lambda function to stream responses/
Currently, Lambda supports response streaming only on Node.js 14.x, Node.js 16.x, and Node.js 18.x managed runtimes. You can also use a custom runtime with a custom Runtime API integration to stream responses.

なお現時点でLambda側ではNode.jsの14.x以降のバージョンのみが対応してしており他のマネージドランタイムでは対応していないようです。

SAM CLIも対応済み

AWS公式ブログの方がSAM使っていたので気になってリポジトリの方を確認したところ、SAM CLIの1.79.0がリリースされており既に本機能に対応していました。

呼び出し方法は限られている

https://aws.amazon.com/jp/blogs/compute/introducing-aws-lambda-response-streaming/
Neither API Gateway nor Lambda’s target integration with Application Load Balancer support chunked transfer encoding. It therefore does not support faster TTFB for streamed responses. You can, however, use response streaming with API Gateway to return larger payload responses, up to API Gateway’s 10 MB limit. To implement this, you must configure an HTTP_PROXY integration between your API Gateway and a Lambda function URL, instead of using the LAMBDA_PROXY integration.

なおALBおよびLambdaプロキシ統合のAPI Gatewayは今回追加されたレスポンス形式に対応していないため現時点では利用できません。

Lambda関数URLの直接アクセス、HTTP統合のAPI Gateway経由の呼び出しといった別の手段でアクセスする必要があります。

https://aws.amazon.com/jp/blogs/compute/introducing-aws-lambda-response-streaming/
You can use the AWS SDK to stream responses directly from the new Lambda InvokeWithResponseStream API. This provides additional functionality such as handling midstream errors. This can be helpful when building, for example, internal microservices. Response streaming is supported with the AWS SDK for Java 2.x, AWS SDK for JavaScript v3, and AWS SDKs for Go version 1 and version 2.

一部のSDKでは今回の応答方式に対応した呼び出しAPIであるInvokeWithResponseStreamが実装されておりそちらを経由して実行することも可能です。

料金

ストリーミングレスポンスは応答サイズに応じて追加料金がかかります。

本記事執筆時点で東京リージョンの料金は$0.008/GBとなるようです。

最新の情報は料金ページをご確認ください。
(執筆時点では英語ページのみに記載有)

設定

本機能を利用するにあたり呼び出しモードの設定が必要となります。

呼び出しモードの変更は関数URLの作成から行うことができます。

設定は作成後にも変更可能なため既存の関数URLを変更することが可能です。

コードサンプル

せっかくSAMも対応しているのでSAM経由でデプロイします。

SAMテンプレート

InvokeModeの値で先ほどの画面相当の設定が可能です。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
  Function:
    Timeout: 15
Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs18.x
      Architectures:
        - arm64
    Metadata:
      BuildMethod: esbuild
      BuildProperties:
        Minify: true
        Target: "es2020"
        Sourcemap: true
        EntryPoints: 
        - app.ts
  StreamingUrl:
    Type: AWS::Lambda::Url
    Properties:
      TargetFunctionArn: !Ref HelloWorldFunction
      AuthType: AWS_IAM
      InvokeMode: RESPONSE_STREAM

Lambdaコード

シンプルに徐々に返却されることがわかるように500ミリ秒ごとにhello worldを返却するコードにしました。
(型はドキュメントから推定しているため誤っている可能性があります)

import {Context} from 'aws-lambda';
import fs from 'fs';

export const lambdaHandler = 
    awslambda.streamifyResponse (
        async (event: JSON, responseStream: fs.WriteStream, context: Context): Promise<void>  => {
            for (let idx = 1; idx < 11; idx++) {
                await new Promise(resolve => setTimeout(resolve, 500));
                responseStream.write(`${idx} times hello world\n`);
            }
            responseStream.write("----END Stream----");
            responseStream.end();
        }
    );

従来と異なり呼び出し元への値の引き渡しがreturn経由ではない点に注意してください。

https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html
The responseStream object is a Node.js writableStream. As with any such stream, you should use the pipeline() method.
...
While responseStream offers the write() method to write to the stream, we recommend that you use pipeline() wherever possible. Using pipeline() ensures that the writable stream is not overwhelmed by a faster readable stream.

今回は一旦WriteStream.write()を直接呼び出していますが、本来はStream.pipeline()を経由しての書き込みが推奨されています。

デプロイ

SAM経由でデプロイでも特別なコマンドは必要なく通常通りsam deployで可能です。

アクセス確認

IAM認証をしようとすると少し手間になるので確認の際に一時的に認証を外してcurlコマンドでアクセスを確認しました。

一気に値が返却されるのではなく徐々に値が返却されていることがわかります。

実のところ公式ブログの表現としてはvia chunked transfer encodingとなっておりTransfer-Encoding: chunkedになることは明記されていませんでしたが--verboseで応答を確認してみると思っていた通り利用が確認できました。

$ curl --verbose https://pmrjgmyjlejct5nzedxb477o3e0gkyar.lambda-url.ap-northeast-1.on.aws/
....
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Sat, 08 Apr 2023 13:14:11 GMT
< Content-Type: application/octet-stream
< Transfer-Encoding: chunked
< Connection: keep-alive
...
<
1 times hello world
2 times hello world
3 times hello world
4 times hello world
5 times hello world
6 times hello world
7 times hello world
8 times hello world
9 times hello world
10 times hello world
* Connection #0 to host pmrjgmyjlejct5nzedxb477o3e0gkyar.lambda-url.ap-northeast-1.on.aws left intact

終わりに

まだ利用できる範囲は狭いですがAWS Lambdaでユーザ側で別の手段を用意することなくストリーミングな応答を利用することができるようになりました。

サーバサイドのみではなくクライアント側としても別の技術を用意する必要がなく通常のHTTP通信の範囲でリアルタイムな処理ができたりと嬉しいアップデートです。

一方でTransfer-Encoding: chunkedによる応答については実はHTTP/1.1までの対応となりHTTP/2では使用が禁止されているという一面があります。
今後のアップデートでどうなって行くのかという部分は見守る必要があるかもしれません。