はじめに
はじめまして.
KDDIアジャイル開発センター株式会社の岩田です.
入社して初めてAWSに触れ,その便利さに日々感動している新卒エンジニアです.
私は入社してからAmplifyを使う機会が多かったので,「インフラをほとんど意識せずにWebアプリを公開できる」というAmplifyの開発体験にすっかりハマっていました.その上,GitHubと連携しておけばPRを作成するだけで自動でプレビュー環境を作ってくれるので,正直「Amplifyって何でもできるんじゃないか」とまで思っていました.
しかし,どんなサービスにも制約はあるもの.今回はNext.js + AIチャットアプリの開発に携わって知ったAmplifyの2つの制約と,その解決策として採用された Lambda Web Adapter について話していきます.
背景
当初のAIチャットアプリの構成は以下でした.(抜粋)
- JSフレームワーク:Next.js
- バックエンド:AWS Amplify
- TypeScript/Reactライブラリ:assistant-ui
- ...
このassistant-uiとは,ChatGPTのようなAIチャットアプリのUIを簡単に構築できるTypeScript/Reactライブラリです.
詳細は省きますが,公式のGetting Startedに書かれた手順を行うだけで,以下のようなチャットアプリが完成します.

制約その1:レスポンスストリーミング
assistant-uiは公式ドキュメントでもまずNext.jsを前提にしたセットアップ手順(npx assistant-ui@latest create)が紹介されており,「Next.js上で動かすこと」を基本ユースケースとして想定しているライブラリです.
Next.jsが用意するAPIルートに以下のように書くことで,assistant-uiと連携したストリーミング対応のチャットAPIエンドポイントが完成します.
import { bedrock } from "@ai-sdk/amazon-bedrock";
import { convertToModelMessages, streamText } from "ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: bedrock("anthropic.claude-3-haiku-20240307-v1:0"),
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}
しかし,いざAmplifyにデプロイしてAIに質問してみると,,,


画像では伝わりにくいですが,AIが数秒考えた後パッと全文が返ってくるようなUXになりました.文章が長ければ長いほど,AIが全文を返してくるまでの時間が長くなります.つまり,Amplify側がレスポンス全体をバッファリングしてしまい,処理が全て完了してから一度にクライアントへ送信される挙動となっています.
これはAWS re:Postにも質問として挙がっており,AWS Amplify上にホストされているNext.jsのAPIルートではレスポンスストリーミングに現在対応していないとのことです.
制約その2:タイムアウト
AIと対話していると発生したもう一つの問題がタイムアウトです.例えば,回答に長い時間が必要な質問をした場合,
このように,途中で力尽きて回答をやめてしまいました.ネットワークタブを見てみると,30秒で通信が途切れていることが分かります.何度回答を生成しても30秒で通信が途切れるため,何かしらのタイムアウトが起きていることがわかります.
Amplify + Next.jsでは呼び出したサーバ処理が30秒を超えた場合,リクエストがタイムアウトエラーとなってしまうようです.開発当時はこの30秒という上限を変更することはできませんでした.公式Githubのイシューにも取り上げられており,依然未対応であることがわかります.
AmplifyからLambda Web Adapterへ
ここまでで挙げた2つの制約はどちらもAmplifyに起因していました.そこで開発中に採用されたのがLambda Web Adapterです.Lambda Web Adapterとは,普通のWebサーバとして動くアプリを格納したコンテナを,そのままLambda上で動かせるようにする拡張機能です.
通常,Lambdaはイベント駆動であり,1リクエストごとにプロセスが起動・終了します.このモデルをそのまま用いた場合,常時稼働するようなWebサーバと相性が良くありません.Lambda Web Adapterはここをうまく橋渡ししており,

- 受け取った Lambda イベントを HTTP リクエストに変換
- コンテナ内で起動している Next.js サーバにプロキシ
- レスポンスを Lambda のレスポンス形式に変換して返却
これらの処理をよしなにやってくれています.
AWS SAMでNext.js + Lambda Web Adapterをデプロイ
Lambda Web Adapterを使うとしても,実際どうやってデプロイするかを考えなければなりません.今回の構成では,こちらの記事やこちらのサンプルを参考にしてAWS SAMを利用してデプロイしました.
これらを元に作成したのが,以下のtemplate.yamlです.(一部抜粋)
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: "Sample Lambda Streaming Response SAM Template using Next.js"
Globals:
Function:
Timeout: 60 # 全Lambda関数のタイムアウト設定(秒)
Resources:
StreamingNextjsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: ./
MemorySize: 512
PackageType: Image
Architectures:
- x86_64
Role: !GetAtt StreamingNextjsFunctionRole.Arn
Environment:
Variables:
AWS_LAMBDA_EXEC_WRAPPER: /opt/extensions/lambda-adapter
NEXT_TELEMETRY_DISABLED: "1"
NODE_ENV: production
FunctionUrlConfig:
AuthType: NONE
InvokeMode: RESPONSE_STREAM # ストリーミングレスポンスモード
Metadata:
DockerTag: v1
DockerContext: ./
Dockerfile: Dockerfile
まず,Next.jsアプリを含むコンテナイメージをビルドし,そのイメージをLambdaにデプロイしています.このコンテナイメージの中にNext.jsのアプリ本体とLambda Web Adapter(/opt/extensions/lambda-adapter) を格納しています.(/opt/extensions/lambda-adapterはDockerfile内で定義)
次に,Lambdaはレスポンスストリーミングに対応しているため,FunctionUrlConfigのInvokeModeをRESPONSE_STREAMとします.こうすることで,Next.jsのAPIルートのレスポンスストリーミングをクライアントまでそのまま流せるようになります.(Bedrockを使用する場合は,適宜InvokeModelWithResponseStreamポリシーの付与が必要)
最後に,Lambda関数はタイムアウト時間を30秒以上に設定できるため,GlobalsのFunction内にあるTimeoutを60秒に設定しています.必要に応じてさらに長く設定できるため,より柔軟な設計になったかと思います.
以上を行うことで,デプロイした環境ではAIからストリームレスポンスを受け取れ,長い回答も安定して出力することが確認できました.

最後に
本記事では,Next.js + assistant-ui を Amplify で動かそうとした際に直面した
- レスポンスストリーミングに非対応
- APIルートのタイムアウト(30秒)
という 2つの「制約」について紹介し,それらを解消するために Amplify から Lambda Web Adapter + AWS SAM へ移行した経緯と,参考にさせていただいた記事・サンプルをまとめました.
Lambda Web Adapter を用いることで,
- Next.jsのレスポンスストリーミングをそのまま活かせること
- Lambdaのタイムアウト設定を変更することで,30秒を超える処理にも対応できること
を確認でき,AIチャットアプリとして「ストリーミングしながら最後まで回答してくれる」構成に近づけられたと思います.
一方で,今回の構成ではLambda Function URLをそのまま公開しているため,セキュリティはかなりガバガバです.Function URLの前段にCloudFrontやAPI Gatewayを置いてWAF/OACを組み合わせることで対応していきたいと考えています.
ここまでに紹介した技術や構成の多くは,社内の先輩が見つけてきた記事や検証結果です.ほとんど「先輩の肩の上」に立って書いた記事ですが,同じような課題にぶつかった方の助けになれば幸いです.
初めての技術記事で拙い部分も多かったかと思いますが,最後までお読みいただきありがとうございました.
今後の記事では,セキュリティや本番運用を意識した改善案の検証結果なども共有していければと思います.

