はじめに
先日、Amazon Bedrock AgentCoreとNext.jsを使って、LLMからの回答をWebUI上でストリーミング表示してみました。
本来は、Next.jsをAmplify Hostingにデプロイして実現したかったのですが、API Routesの部分が上手くいかず...
結局、Vercelにデプロイして動くようにしました。
結論としては、AWS公式に「ストリーミングはサポートしてない」と書かれていたのですが、せっかくなのでAmplify Hosting上でNext.jsのAPI Routesがどのような環境で動作しているのか調査してみることにしました。
背景
やりたかったこと
本来は、Amplify HostingにNext.jsをデプロイして、API Routes(下図のバックエンドAPI)とAgentCoreと連携させ、サーバー送信イベント(SSE)によるストリーム通信を実現したかったです。
AgentCoreはサーバー送信イベント(SSE)のストリーミングに対応
下記ドキュメントにもある通り、AgentCoreでストリーミングを実現するには、サーバー送信イベント(SSE)を使用する必要がありました。
参考:https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/runtime-service-contract.html
苦戦したところ
AgentCoreからバックエンドAPIへのレスポンスはストリーミングで返ってくるのですが、API Routesからフロントエンドへはストリーミングで返ってこないという問題にぶち当たりました。
Vercelにデプロイして解決
そこでAmplify Hostingを諦めて、Vercelにデプロイしたところ、期待通りストリーミングでレスポンスが返ってくることが確認に出来ました。
AWS Amplify Hostingのドキュメントを確認してみる
Amplify Hostingのドキュメントをよくよく見ると...
そもそもストリーミングがサポートされていませんでした。
参考:https://docs.aws.amazon.com/ja_jp/amplify/latest/userguide/ssr-amplify-support.html
解散!!
というのも、つまらないのでもう少し調査してみます。
API Routesの動作環境を調査してみる
先に結論
Amazon ClouFrontのオリジンに関数URLを付与したLambdaが設定されており、カスタムランタイム上のnode18でAPI Routesのコードが動作してそう。
これらのリソースはAWS管理のようで、マネジメントコンソール上などからは確認できない。
事前準備: Amplify Hostingにアプリをデプロイする
まずは環境を構築する必要があるため、Amplify Hostingに以前作ったNext.jsのアプリをデプロイします。
準備1. Amplify Hostingへのデプロイ用に設定ファイルを作成する
Amplify Hostingへデプロイするために、以下のamplify.yml
ファイルを作成しました。コードはGitHubにPushしておきます。
amplify.ymlの内容はこちら
version: 1
applications:
- appRoot: nextapp
backend:
phases:
build:
commands:
- npm ci --cache .npm --prefer-offline
- npx ampx pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- touch .env.production
- echo "AGENT_CORE_ENDPOINT=$AGENT_CORE_ENDPOINT" >> .env.production
- export NEXT_TELEMETRY_DISABLED=1
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
- .next/cache/**/*
Amplify Hostingでビルド設定をカスタマイズする
Amplify Hostingは、デプロイ時にアプリ内のpackage.json
ファイルから構成を読み取って、いい感じにデプロイしてくれるようです。
ただし、YAMLファイル
を作成して、ビルド構成をカスタマイズすることもできます。
今回は、再現性という観点から一応YAMLファイルを作成して、ビルド設定を明示的に定義することにしました。
準備2. マネジメントコンソール上からデプロイする
コードの準備は整ったので、マネジメントコンソール上にアクセスして、Amplify Hostingでデプロイします。
詳細はこちら
-
該当のリポジトリとブランチを選択します
-
また今回のアプリ構成はモノレポなので、「私のアプリケーションはモノレポです」にチェックを入れます
-
環境変数を設定したら、再デプロイします
調査1. マネジメントコンソールのリソースを確認
まず初めにマネジメントコンソール上から作成されたリソースが無いか確認しました。
- AWS Lambda
- Amazon EC2
- Amazon ECS
とりあえず主要なコンピューティングサービス周りを見てみましたが、特に作成されていませんでした。
よって、AWS管理のリソースが作成されると推察しました。
調査2. AWS Lambdaの環境変数を確認してみる
AWS Amplifyは、サーバレスリソースしか作成しないでしょう。ということで、AWS Lambdaと仮定します。
準備1. コードにログを仕込んで再デプロイする
API Routesのコードに下記のコードを追加して、ログを出力するようにします。
後述しますが、リクエストヘッダーも見たいので、ログに追加してます。
function logLambdaEnvInfo(request: NextRequest) {
// Lambda環境変数の内容(キーと値のペア)をすべてログ出力
console.log('[Lambda Env] All env:', process.env);
// リクエストヘッダー全体をログ出力
const headersObj = Object.fromEntries(request.headers.entries());
console.log('[Lambda Env] Request headers:', headersObj);
}
準備2. CloudWatch からログを取集する
マネジメントコンソール上から「ホスティングしているコンピューティングログ」を開くと、Amazon CloudWatch のリンクがあるので、リンクをクリックしてログストリームを表示します。
あとはデプロイしたアプリを操作するとログが吐かれるので、そのログを収集します。
ログを確認する
それでは収集したログを確認していきます。まずは環境変数です。
以下に環境変数の抜粋しました。概要は生成AIにまとめてもらいました。
環境変数名 | 値 | 概要 |
---|---|---|
AWS_LAMBDA_FUNCTION_NAME |
Compute-d2l408rai9nvda-655b54c79442f289d521d4cf93d289d8 |
実行中のLambda関数の名前。 |
AWS_LAMBDA_FUNCTION_VERSION |
$LATEST |
デプロイされた関数のバージョン。$LATEST は最新版を示す。 |
AWS_LAMBDA_RUNTIME_API |
127.0.0.1:9001 |
ランタイムAPIのエンドポイント。カスタムランタイムがLambdaと通信するために使用。 |
AWS_EXECUTION_ENV |
AWS_Lambda_nodejs18.x |
実行環境の識別子。ここではNode.js 18.xランタイムを使用していることを示す。 |
AWS_LAMBDA_INITIALIZATION_TYPE |
on-demand |
初期化タイプ。on-demand はリクエスト時に初期化されることを意味する。 |
AWS_LAMBDA_EXEC_WRAPPER |
/opt/wrapper |
関数実行時に使用されるラッパースクリプトのパス。通常は拡張機能やモニタリング用に使われる。 |
ポイント1. API RoutesはAWS Lambdaで実行されている
環境変数の取得が可能で、AWS Lambda関連の設定が確認できたので、API Routesは予想通りAWS Lambda上で実行されてるようです。
クライアント -> AWS Lambda
ポイント2. 実行環境は カスタムランタイムで実行されている
以下のとおり、AWS_LAMBDA_RUNTIME_API
という環境変数が確認できました。
つまり、実行環境ではランタイムAPI
が使われており、ランタイムAPIが使われているということはカスタムランタイム
で動作してそうです。
AWS_LAMBDA_RUNTIME_API: '127.0.0.1:9001'
ランタイム
は、コードの実行環境のことであり、AWSが提供する標準のランタイム
と自分で実行環境を構築するカスタムランタイム
があります。
ランタイムAPI
は、AWS Lambdaのサービスと 実際にコードを動かす実行環境との橋渡しをするためものです。
参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-custom.html
標準のランタイムを使用する場合には、橋渡しを良しなにAWS側でやってくれますが、カスタムランタイムの場合は、自前でランタイムAPIを設定しないといけないようです。
また以下の環境変数からランタイムはnode.jsの18系を使用しています。
AWS_EXECUTION_ENV: 'AWS_Lambda_nodejs18.x'
ただし、AWSのドキュメントを確認すると、Node.js 18はLambdaの標準のランタイムでサポートされています。ただし、2025/9/1で廃止とあります。
じゃあ、標準のランタイムで動かせばいいじゃん!と思いましたが、こういったEOLを見越して、カスタムランタイムを使ってるのですかね...
ポイント3. オンデマンド実行なのでコールドスタートが発生しそう
AWS_LAMBDA_INITIALIZATION_TYPE
を見るとon-demand
とあります。
AWS_LAMBDA_INITIALIZATION_TYPE: on-demand
この設定だと、リクエストが来たタイミングでLambdaが起動されるため、一定時間アクセスがないと、次回のリクエスト時に初期化処理が走り、コールドスタートが発生します。
このあたりは、実際に運用するとパフォーマンス周りでハマりそうですね...
provisioned-concurrency を設定すれば、常にLambdaを起動状態に保つことができ、コールドスタートを回避できます(ただしコストは増加)。
調査3. リクエストヘッダーを確認する
リクエストヘッダーも確認してみます。生成AIにまとめてもらいました。
Amazon CloudFront系のヘッダー
ヘッダー名 | 値 | 概要 |
---|---|---|
cloudfront-forwarded-proto |
https |
CloudFrontが使用したプロトコル。 |
cloudfront-viewer-http-version |
3.0 |
使用されたHTTPバージョン。 |
cloudfront-viewer-tls |
TLSv1.3:TLS_AES_128_GCM_SHA256:connectionReused |
TLSバージョンと暗号スイート。 |
cloudfront-is-android-viewer |
false |
Android端末かどうか。 |
cloudfront-is-desktop-viewer |
true |
デスクトップ端末かどうか。 |
cloudfront-is-ios-viewer |
false |
iOS端末かどうか。 |
cloudfront-is-mobile-viewer |
false |
モバイル端末かどうか。 |
cloudfront-is-smarttv-viewer |
false |
スマートTVかどうか。 |
cloudfront-is-tablet-viewer |
false |
タブレット端末かどうか。 |
cloudfront-viewer-asn |
2518 |
Autonomous System Number(ISP識別子)。 |
cloudfront-viewer-country |
JP |
国コード(ISO 3166-1 alpha-2)。 |
cloudfront-viewer-country-name |
Japan |
国名。 |
cloudfront-viewer-country-region |
01 |
地域コード。 |
cloudfront-viewer-time-zone |
Asia/Tokyo |
タイムゾーン。 |
※クライアントの位置情報など含まれるので、以下のヘッダーは除外しました。
-
cloudfront-viewer-address
: クライアントのIPアドレスとポート -
cloudfront-viewer-country-region-name
: 地域名 -
cloudfront-viewer-city
: 推定される都市名 -
cloudfront-viewer-postal-code
: 郵便番号 -
cloudfront-viewer-latitude
: 推定緯度 -
cloudfront-viewer-longitude
: 推定経度
そのほかヘッダー
ヘッダー名 | 値 | 概要 |
---|---|---|
accept-encoding |
gzip, deflate, br, zstd |
クライアントが対応する圧縮方式。 |
accept-language |
ja,en-US;q=0.9,en;q=0.8 |
クライアントが優先する言語。 |
content-length |
31 |
リクエストボディのサイズ(バイト)。 |
host |
amplify-hosting-debug.1234fsdwf211as.amplifyapp.com |
リクエスト先のホスト名。 |
origin |
https://amplify-hosting-debug.1234fsdwf211as.amplifyapp.com |
リクエスト発信元のオリジン。 |
priority |
u=1, i |
HTTP/3の優先度情報。 |
referer |
https://amplify-hosting-debug.1234fsdwf211as.amplifyapp.com/ |
リクエスト元のページURL。 |
sec-ch-ua |
"Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139" |
ブラウザのブランド情報(User-Agent Client Hints)。 |
sec-ch-ua-mobile |
?0 |
モバイル端末かどうか(User-Agent Client Hints)。 |
sec-ch-ua-platform |
"Windows" |
プラットフォーム情報(User-Agent Client Hints)。 |
sec-fetch-dest |
empty |
リクエストの目的(例:script, imageなど)。 |
sec-fetch-mode |
cors |
リクエストのモード(CORSなど)。 |
sec-fetch-site |
same-origin |
リクエスト元と送信先の関係。 |
user-agent |
Mozilla/5.0 (Windows NT 10.0; Win64; x64)... |
クライアントのブラウザとOS情報。 |
via |
3.0 ...cloudfront.net (CloudFront) |
経由したプロキシ情報。 |
x-amz-cf-id |
qAbvTiLFmi4Wr3Nb2fAI9oRSAfNt_3wto5zNAwBV70Ofk0FKx7GaIQ== |
CloudFrontのリクエストID。 |
x-amzn-trace-id |
Root=1-68b2dada-... |
AWS X-RayのトレースID。 |
x-forwarded-for |
118.108.78.0, 64.252.67.70 |
クライアントの元IPアドレス。 |
x-forwarded-host |
amplify-hosting-debug.1234fsdwf211as.amplifyapp.com |
元のホスト名。 |
x-forwarded-port |
443 |
元のポート番号。 |
ポイント1. AWS Lambdaの前段にはAmazon CloudFrontがいる
前述した通り、リクエストヘッダーにcloudfront-xxx
が含まれていました。
これらはAmazon CloudFrontが付与するものなので、AWS Lambdaの前段にAmazon CloudFrontがいると思われます。
クライアント -> Amazon CloudFront -> AWS Lambda
ポイント2. AWS Lambdaには関数URLが設定されている
Amazon CloudFrontのオリジンに設定できるのは、関数URLを設定したAWS Lambdaになります。
そのため、今回のLambda 関数には、関数URLが設定されていると考えられます。
感想
今回は、私自身あまりAmplify Hostingについて詳しくないですが、自分なりに調べてみました。
やはりAmplifyを使うと楽ちんにアプリを構築できる反面、裏の仕組みをしっかり理解していない、運用中にハマりそうだなと思いました...
(何かあったら、裏側の動作仕様を検証するところからね...)
とはいえ、小規模なアプリに使う分にはサクッと構築できて重宝すると思います。
今後、もう少しAmplifyと仲良くなれるように、色々と使っていきたいと思います。