🔌

Lambda Web Adapter を利用してSSR Streamingを試してみた

2023/12/03に公開

こちらは、ラクスパートナーズ Advent Calendar 2023の5日目の記事です!🎄📅🎁
https://qiita.com/advent-calendar/2023/rakus-partners
前日の記事は、「Next.js App RouterとMDXでリッチなスキルシートをサクッと書く」 でした!

📌 はじめに

こんにちは!@Ryo54388667です!☺️

普段は都内でフロントエンドエンジニアとして業務をしてます!
主にTypeScriptやNext.jsといった技術を触っています。

Next.js製アプリのホスティング先をリサーチしていた時、このLambda Web Adapterという技術を見かけました。

https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter

https://aws.amazon.com/jp/blogs/news/implementing-ssr-streaming-on-nextjs-with-aws-lambda-response-streaming/

Next.js製アプリをVercel以外でホスティングを検討している人の参考になれば幸いです。

📌 Lambda Web Adapter とは

概要

初見の印象としては、
「そんなことできるん!?」
でした。驚き!😳

通常、Lambda は特定のイベントに応じて動作するという固有の特性を持っています。
LambdaではWebサーバーが行うようなことは対応していないと思っていました。
しかし、このLambda Web Adapterを利用するとWebサーバーのような挙動を実現することができます。すごい!!

端的にいうと、
Lambdaが受け取る固有のイベントをHTTPリクエストに変換する役割を担っています。

Lambda に統合された API Gateway や ALB がリクエストを受信した際、下図の流れで処理されます。図中の番号に沿って説明します。

  • Lambda ランタイムが統合先 (API Gateway, ALB など) からイベントを受信します。
  • イベントは Lambda Extension である Web Adapter により処理されます。
  • Web Adapter はイベントをパース・HTTP リクエストに変換し、ウェブアプリのプロセスに HTTP で転送します。
  • ウェブアプリのプロセスは HTTP で受信したリクエストを処理し、HTTP でレスポンスを返します。これにより、ウェブアプリは Lambda を意識せず従来のコードのまま動作すればよいのです。

Web Adapter は受け取ったレスポンスを API Gateway が期待する形式に変換し、Lambda ランタイムに返します。Lambda ランタイムは統合先にレスポンスを返します。
https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/?awsf.filter-name=*all

仕組みは理解できるのですが、「ほんとに大丈夫なん?」という不安があります。Lambda一本でWebアプリを運用してもよいものなのか?と疑問に感じます。少なくとも自分の観測範囲ではこの技術を利用してPrd環境を運用しているという事例を聞いたことがありません😇

そこで、利用するメリットと具体的なユースケースについて詳しく調べてみました。

メリットとユースケース

以下に、利用するメリットと具体的なユースケースについて列挙していきます。

  1. サービスをMinimumスタートしたいケース。 EC2等のクラウドのサーバーを利用する際、アクセスがない状態でも課金されます(時間課金)。そうなると、まだサービスの立ち上げ段階でユーザーが少ないなか、ランニングコストが、かさんでいくのはツラいです。。そこでピッタリなのが、Lambdaの課金システムです。利用したぶん(処理時間)だけの課金。いわゆる、Pay as you go です。

  2. サービスの急拡大が予見されているケース。 例えば、広告に回せるような資金があり、サービスの拡大が見込まれる場合が該当するのかと。ここで役立つのが、スケーラビリティとポーダビリティです。この技術は、急なアクセスに対しても、フルマネージドでスケーリング処理をしてくれます✨これはありがたい。これがない場合は、自分でELBとコンテナのオートスケーリングの設定を行う必要があります。また、ポーダビリティに関しては、この技術でもDockerfileを利用するので、のちにAWS Fargateに移行する時も少しの変更で行うことができます。移行コストの工数を低減できるのもメリットです。

  3. サービスの一部機能のみを別々に管理したいケース。 例えば、一部の機能だけアクセス数が多い、もしくは、試験的な機能を導入するような場合に、この技術が活かせるかと思います。柔軟に対応できるメリットを享受できます。

こちらも参考になります!
https://speakerdeck.com/_kensh/web-frameworks-on-lambda?slide=36

📌 SSR Streaming とは

概要

SSRの問題点を解決するような新しいレンダリング技術。

  • HTML を送信する前に、すべてのデータがサーバーに読み込まれるのを待つ必要はなくなりました。代わりに、アプリのシェルを表示するのに十分な情報が得られたらすぐに HTML の送信を開始し、準備ができたら残りの HTML をストリーミングします。
  • ハイドレートを開始するためにすべての JavaScript が読み込まれるのを待つ必要はもうありません。代わりに、コード分割とサーバー レンダリングを併用できます。サーバーの HTML は保持され、関連するコードが読み込まれるときに React がそれをハイドレートします。
  • ページとの対話を開始するために、すべてのコンポーネントがハイドレートされるのを待つ必要はなくなりました。代わりに、選択的ハイドレーションを利用して、ユーザーが操作しているコンポーネントに優先順位を付け、早期にハイドレーションを行うことができます。

https://github.com/reactwg/react-18/discussions/37

SSR Streaming は概要の説明だけでひと記事になりそうなので割愛します😇
詳しくは引用先を参照してください。

📌 ハンズオン

パッケージとバージョン

このハンズオンでは、以下のパッケージとバージョンを使用します。

パッケージ バージョン
next 13.3.0
react 18.2.0

手順

Next.js アプリケーションを作成するために、以下のコマンドを使用します。
AWS Lambda Web AdapterのNext.jsのレスポンスストリーミングの例をベースとしてプロジェクトを作成します。公式が公開してくれています!ありがたし👏

npx create-next-app@latest <PJ名> --use-npm --example "https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/nextjs-response-streaming"

AWS Lambda Web Adapterにとって重要なのは、PJ内の/Dockerfileの下記の一行です。
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter

次に、next.config.js ファイルを設定します。
output: "standalone"を追加してください。
詳しくはこちらのページを見てください。

/** @type {import('next').NextConfig} */
const nextConfig = {
  experimental: {
    appDir: true,
  },
  compress: true,
  output: "standalone", // 追加
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com"
      }
    ]
  }
}

module.exports = nextConfig

次に、SAM (Serverless Aplication Model) を利用してデプロイします。
SAMのセットアップは下記の記事がわかりやすかったです!
https://qiita.com/yh1224/items/f3e22b886639e2605f2f

SAMのセットアップ後、
Docker Desktop を起動し 、以下のコマンドを実行してビルドします。
--use-container オプションは、Docker コンテナ内でビルドを行うことを指定します。

//terminal
sam build --use-container

準備が終わったので、次に、sam deploy --guided コマンドを使用します。
これにより、Q&A形式でデプロイプロセスをガイドしてくれます。

1回目のデプロイ
sam deploy --guided

2回目以降のデプロイでは下記のコマンドのみでOKです。
sam deploy 

以下は、sam deploy --guided コマンドの実行中に表示されるガイドの一例です。
スタック名、リージョン、IAMロールの作成許可など、デプロイに必要な情報を尋ねてくれます!
ありがたし。

//terminal
 Stack Name [sam-app]: next-lambda-streaming
        AWS Region [ap-northeast-1]: ap-northeast-1
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: n
        Capabilities [['CAPABILITY_IAM']]: 
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: y
        StreamingNextjsFunction Function Url has no authentication. Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: Y
        SAM configuration file [samconfig.toml]: samconfig.toml
        SAM configuration environment [default]: default
        Create managed ECR repositories for all functions? [Y/n]: Y

最終的に、ターミナルにデプロイされたアプリケーションのURLが表示されます。
このURLを使用して、デプロイされたアプリケーションにアクセスできます。
問題なく動作していそうです!🎉

📌 SSR Streamingかどうか確認

レスポンスヘッダーについて

Neither API Gateway nor Lambda’s target integration with Application Load Balancer support chunked transfer encoding.
https://aws.amazon.com/jp/blogs/compute/introducing-aws-lambda-response-streaming/

とあるので、正しくStreamingできていれば、Lambdaのレンスポンスヘッダーに
Transfer-Encoding: chunked
のプロパティが存在するはずなので、こちらの有無を確認します。

こちらも存在することがわかります!

Suspenseのfallbackに指定されたローディングコンポーネントについて

React18.x で導入されたSuspenseコンポーネントには、propsのfallbackにローディング等のために利用するコンポーネントを指定することができます。(もちろんローディング以外の意図として利用することも可能です。)こちらが正しく機能しているかどうかを確認します。

こちらについても、動画を見ると、スケルトンローディングが意図通り表示されています。

TTFB(Time To First Byte)について

レスポンスストリーミングでは、準備が整った時点で部分的なレスポンスをクライアントに送り返し、TTFB レイテンシーをミリ秒以内に改善することができます。
https://aws.amazon.com/jp/blogs/news/introducing-aws-lambda-response-streaming/

との記述があるので、TTFB指標を確認します。
具体的な方法としては、Chromeのデベロッパーツール(Networkタブ > Timingセクション)を利用します。下記の画像のような箇所です。

参考:Waiting (TTFB)について

Waiting (TTFB). The browser is waiting for the first byte of a response. TTFB stands for Time To First Byte. This timing includes 1 round trip of latency and the time the server took to prepare the response.

https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation

SSR Streaming に対応していないケースと比較をしたいので、今回はAmplifyでデプロイしたものと比較します。AmplifyではまだSSR Streaming は未対応のようです。2つの方法でデプロイしたアプリのwaiting for server responseの時間(ms)を確認します。

確認した結果

Ampify Lambda Web Adapter
平均値 1361 710.977
中央値 1330 679.56

※ 単位: ミリ秒(ms)

計測データ(10回)
Ampify Lambda Web Adapter
1 1500 974.13
2 1300 1060
3 1320 455.56
4 1230 1090
5 1460 919.7
6 1340 402.2
7 1350 671.36
8 1180 687.76
9 1740 521.57
10 1190 327.49

今回、自分の環境だと、AmplifyよりもLambda Web Adapter のほうがTTFBの値がすこぶる良いという結果になりました。個人的にはこの点が非常に驚きでした!爆速!🚀これだけパフォーマンスに差分があるんですね〜

以上より、SSR Streamingになっているかと思います👌

📌 まとめ

Lambda Web Adapterを利用してSSR Streamingを試してみました!
課題はありますが、一考の価値はあると思います。何より、TTFBが爆速です! ここが大きなメリットです!

書き散らしスクラップ

最後まで読んでいただきありがとうございます!
気ままにつぶやいているので、気軽にフォローをお願いします!🥺

https://twitter.com/Ryo54388667/status/1730375251593499133

Discussion