AWS Lambda with Rustでレスポンスストリーミング
Introduction
2023年4月にAWS Lambdaがレスポンスペイロードストリーミングのサポートを発表しました。
この機能を使うことで、レスポンスデータが利用可能になった時点で
呼び出し元にデータを送信することができます。
その結果、パフォーマンスの向上やLambdaの6MB制限を超えたデータを送信できます。
今回はレスポンスストリーミングをRustで実装し、
大きめの画像を返してみます。
Environment
- OS : MacOS 13.0.1
- rust : 1.71.0
AWSアカウントは設定済みとします。
Setup
ではまず、AWS LambdaをRustで実装してみましょう。
AWS Lambda with Rustを実装するにはcargo-lambdaを使うのが簡単です。
cargoをつかってcargo-lambdaをインストールしましょう。
通常はHomebrewで問題ないみたいです。
% brew tap cargo-lambda/cargo-lambda % brew install cargo-lambda
私の場合、cargo installでソースからビルドしました。
% cargo install cargo-lambda
次にプロジェクトの作成です。
newサブコマンドでプロジェクトを作成します。
--httpオプションをつけて関数URLとセットで使えるようにしておきます。
% cargo lambda new large-object-streaming --http % cd large-object-streaming
ビルドとデプロイも簡単です。
↓のようにすれば簡単にできます。
% cargo lambda build --release % cargo lambda deploy
サイズがでかくなりがちなので--releaseつけます。
デプロイが完了したら、AWSコンソールでLambda→関数→作成した関数と進み、
設定→関数URLと選択します。
とりあえず認証タイプはNONEでよいので、関数URLを割り当てましょう。
「https://<発行された関数URK>/?name=streaming!」に
ブラウザでアクセスすれば、
Hello streaming!, this is an AWS Lambda HTTP request
と画面に表示されます。
また、ローカルで動作を確認したい場合、watchコマンドでLambda関数を待ち受け、
invokeサブコマンドを実行します。
% cargo lambda watch ・・・ % cargo lambda invoke --data-file "payload.json" {"statusCode":200,"headers":{"content-type":"text/html"},"multiValueHeaders":{"content-type":["text/html"]},"body":"Hello syuta, this is an AWS Lambda HTTP request","isBase64Encoded":false}
payload.jsonはLambdaに渡すパラメータファイルです。
ここでは下記のように定義しました。
{ "queryStringParameters":{ "name":"syuta" } }
実際にデプロイされているLambdaを呼び出したい場合、
--remoteを指定して実行します。
% cargo lambda invoke --remote --data-file "payload.json" large-object-streaming
S3にbucketと画像を準備
もう一点、S3に適当なbucketを作成し、
1メガくらいの画像と10メガくらいの画像をアップロードしておきます。
これで準備はOKです。
response payload streamingを試してみましょう。
Try
まずは普通にS3から画像を取得してLambdaで返してみます。
まずはCargo.tomlに依存ライブラリを追加。
[dependencies] lambda_http = "0.8.1" lambda_runtime = "0.8.1" tokio = { version = "1", features = ["macros"] } tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", default-features = false, features = ["fmt"] } aws-config = "0.56.0" aws-sdk-config = "0.29.0" aws-sdk-s3 = "0.29.0" anyhow = "1.0.75" serde_json = "1.0.105"
そしてmain.rsにS3の画像を返すコードを記述します。
use aws_config::meta::region::RegionProviderChain; use lambda_http::{run, service_fn, Body, Error, Request, RequestExt, Response}; use aws_sdk_s3::operation::get_object::GetObjectOutput; use aws_sdk_s3::Client; //Get Object from S3 async fn get_object( client: &Client, bucket_name: &str, key: &str, ) -> Result<GetObjectOutput, anyhow::Error> { let object = client .get_object() .bucket(bucket_name) .key(key) .send() .await?; Ok(object) } //lambda function async fn function(event: Request) -> Result<Response<Body>, Error> { let region_provider = RegionProviderChain::default_provider().or_else("ap-northeast-1"); let config = aws_config::from_env().region(region_provider).load().await; let client = Client::new(&config); let bucket = "<your bucket>"; let key = "<image file name>"; let result = get_object(&client,&bucket,&key).await; let image = result.unwrap().body.collect().await; let body = Body::from(image.unwrap().to_vec()); let resp = Response::builder() .status(200) .header("content-type", "image/jpeg") .body(body) .map_err(Box::new)?; Ok(resp) } #[tokio::main] async fn main() -> Result<(), Error> { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_target(false) .without_time() .init(); run(service_fn(function)).await }
ここではLambda実行時に、S3からオブジェクト(さきほどupした画像)を
取得して返しています。
1メガの画像であれば普通に表示できますが、10メガの画像はInternal Server Errorになります。
これが6MBの壁ですね。
Streamingレスポンスを使う
ではStreamingで6MB以上のデータを返せるようにしてみます。
変更点は2つで、
runではなくlambda_runtime::run_with_streaming_response
を使うのと、handler関数の引数をLambdaEvent<Value>にすることです。
use lambda_runtime::{Error, LambdaEvent}; use serde_json::Value; use aws_sdk_s3::operation::get_object::GetObjectOutput; use aws_sdk_s3::Client; use aws_config::meta::region::RegionProviderChain; use lambda_http::{service_fn, Body, Response}; async fn function_handler_stream(_event: LambdaEvent<Value>) -> Result<Response<Body>, Error> { //中身は先程のfunctionと同じ } #[tokio::main] async fn main() -> Result<(), Error> { tracing_subscriber::fmt() .with_max_level(tracing::Level::INFO) .with_target(false) .without_time() .init(); lambda_runtime::run_with_streaming_response(service_fn(function_handler_stream)).await }
ここで少し注意するのがhandlerの引数です。
べつに引数使わないしLambdaEvent<String>にでもしときゃいいだろ、
とかしてたらURLアクセス時に画像が表示されず0バイトのファイルがダウンロードされる現象になったので、
ちゃんとLambdaEvent<serde_json::Value>を引数に指定しましょう。
そして関数URLの呼び出しモードをRESPONSES_STREAMに設定します。
ビルド&デプロイして再度10メガファイルを取得してみましょう。
% cargo lambda build --release % cargo lambda deploy
今度はブラウザに画像が表示されます。
これで20メガまでは取得可能です。(ソフトリミット)
Summary
今回はRustでStreaming Responseを試してみました。
これで微妙にサイズの大きいデータでも面倒な回り道せずに
Lambdaだけで処理できますね。