0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Amazon API Gateway】ニッチかな?REST APIのレスポンスを少しずつ返すストリーミングをやってみる

0
Posted at

はじめに

こんばんは、mirukyです。

今日は比較的ニッチなことをやります。

数秒かかるAPIでは、最後まで何も返らない場合と、完成した部分から返る場合で待ち時間の見え方が変わりますよね。実はAmazon API GatewayのREST APIでは、バックエンドから届いたレスポンスを少しずつクライアントへ転送できるんです。

今回は東京リージョンでLambdaとREST APIを作り、4つの文字列が1秒おきに届くか確かめます。AWSリソースの作成から後片付けまで、AWSマネジメントコンソールだけで進めました。

目次

  1. レスポンスストリーミングの仕組み
  2. Lambda関数を作る
  3. REST APIを作る
  4. 少しずつ届くか確かめる
  5. 実際のAPIへ入れる前に確認すること

1. レスポンスストリーミングの仕組み

通常のBUFFEREDは、API Gatewayがバックエンドのレスポンス全体を受け取ってからクライアントへ返します。STREAMは、受け取ったチャンクを順次転送します。

転送モード クライアントへ返し始める時点 向いている処理
BUFFERED レスポンス全体が完成した後 小さく短い通常のAPI
STREAM 最初のチャンクを受け取った後 生成中の文章、進捗、大きな応答

利用できるのはREST APIのAWS_PROXYまたはHTTP_PROXY統合です。Lambdaプロキシ統合では、API GatewayがLambdaのInvokeWithResponseStreamを使います。HTTP APIにはこの設定がありません。

API Gatewayのレスポンスストリーミングは2025年11月に全AWSリージョンで提供され、Lambdaのレスポンスストリーミングも2026年4月に全商用リージョンへ拡大されました。今回の構成は次のとおりです。

項目 今回の設定
リージョン アジアパシフィック(東京)ap-northeast-1
API Regional REST API
メソッド GET /stream
統合 Lambdaプロキシ統合、STREAM
認証 NONE。検証中だけ使い、確認後すぐ削除
Lambda Node.js 24.x、タイムアウト15秒

1つの応答は最大15分です。アイドルタイムアウトはRegionalとPrivateが5分、Edge-optimizedが30秒となります。STREAMでは、API Gatewayのエンドポイントキャッシュ、コンテンツ圧縮、レスポンスのVTL変換を使えません。

2. Lambda関数を作る

Lambda、API Gateway、IAM、CloudWatch Logsの作成・更新・削除権限と、Lambda実行ロールを渡すiam:PassRoleが必要です。今回の検証は数回のリクエスト、128MBのLambdaを1分未満、数KBのログに収まりました。無料利用枠を考慮しない保守的な試算でも0.001 USD未満ですが、利用中の設定や料金改定によって変わります。

まず、作成方法と関数名を決めるため、関数作成フォームの上部を確かめます。

一から作成でqiita-stream-20260811-r2を入力したLambda関数作成フォーム

作成方法は「一から作成」で、関数名には「qiita-stream-20260811-r2」が入力されています。

続けて、ランタイムと最初の実行権限を確かめます。

Node.js 24.xと新しい実行ロールの説明が見えるLambda関数作成フォーム

ランタイムは「Node.js 24.x」で、「アクセス許可」にはCloudWatch Logsへログをアップロードする権限を持つ実行ロールの説明が表示されています。

ストリームが閉じるまで処理できるよう、一般設定の編集内容を確かめます。

Lambdaのタイムアウトを0分15秒にした一般設定の編集画面

タイムアウト欄は0分15秒で、専用の実行ロールが選択されています。

保存が反映されたか、一般設定の要約を見ておきます。

保存後のLambda一般設定でタイムアウトが15秒になった画面

保存後の「一般設定」には、タイムアウトが0分15秒と表示されています。

index.mjsは次の内容に置き換えます。

import { Readable } from "node:stream";
import { pipeline } from "node:stream/promises";

const wait = (milliseconds) =>
  new Promise((resolve) => setTimeout(resolve, milliseconds));

export const handler = awslambda.streamifyResponse(
  async (_event, responseStream) => {
    const metadata = {
      statusCode: 200,
      headers: {
        "Content-Type": "text/plain; charset=utf-8",
        "Cache-Control": "no-cache",
        "Access-Control-Allow-Origin": "*",
      },
    };

    responseStream = awslambda.HttpResponseStream.from(
      responseStream,
      metadata,
    );

    // 1秒ごとに4つのチャンクを返す
    const chunks = Readable.from(
      (async function* () {
        for (let number = 1; number <= 4; number += 1) {
          yield `chunk=${number}\n`;
          if (number < 4) {
            await wait(1000);
          }
        }
      })(),
    );

    await pipeline(chunks, responseStream);
  },
);

streamifyResponse()でストリーミング対応のハンドラーにし、HttpResponseStream.from()でHTTPステータスとヘッダーを先に渡します。pipeline()は4つのチャンクを最後まで書き込み、ストリームを閉じる役割を持ちます。今回は別オリジンの検証用HTMLから呼び出すため、CORSヘッダーも返します。

更新後のコードの冒頭とハンドラーを確かめます。

ReadableとpipelineをimportしstreamifyResponseを使うLambdaコード

更新後のコードソースには、Readableとpipelineのimport、およびstreamifyResponseを使うハンドラーが表示されています。

APIへつなぐ前に、関数が最後まで動くか同期呼び出しで確かめます。

同期呼び出しのstreamTestR2を作るLambdaテストイベント

テストイベントは同期呼び出しで、イベント名は「streamTestR2」です。

このテストは関数の成功確認に使います。

Lambdaコンソールの成功した実行結果に4チャンクがまとめて表示された画面

Lambdaコンソールの実行結果は成功で、Response欄にはchunk=1からchunk=4までがまとまって表示されています。Lambdaの公式ドキュメントのとおり、コンソールからの呼び出し結果はバッファーされるため、これだけでは途中受信を判定できません。

3. REST APIを作る

API GatewayでRegional REST APIを作り、/streamリソースにGETメソッドを追加します。API名と統合先のLambda関数名はqiita-stream-20260811-r2です。

Lambdaから受け取ったチャンクをそのまま転送するため、完成した統合設定を確かめます。

Lambdaプロキシ統合をTrueにしレスポンス転送モードをストリームにしたAPI Gatewayの統合設定

統合リクエストの設定では、統合タイプが「Lambda」、Lambdaプロキシ統合が「True」、レスポンス転送モードが「ストリーム」です。

次に、クライアントから呼ぶためのステージを確かめます。

demoステージの呼び出しURLとアクティブなデプロイを示すAPI Gateway画面

ステージ名は「demo」で、呼び出しURLとアクティブなデプロイが表示されています。今回のメソッド認証はNONEです。公開URLを知っていれば誰でも呼び出せるため、動作確認が終わるまでの一時設定として扱います。

4. 少しずつ届くか確かめる

ブラウザーで途中受信を見るため、次のHTMLをstreaming-client.htmlとして保存します。endpointは、ステージの呼び出しURLの末尾に/streamを付けた値へ置き換えてください。

<!doctype html>
<meta charset="utf-8">
<title>Streaming response check</title>
<pre id="log"></pre>
<script type="module">
// デプロイ後の呼び出しURLへ置き換える
const endpoint = "https://API_ID.execute-api.ap-northeast-1.amazonaws.com/demo/stream";
const log = document.querySelector("#log");
const started = performance.now();
const response = await fetch(endpoint);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffered = "";

while (true) {
  const { value, done } = await reader.read();
  if (done) break;
  buffered += decoder.decode(value, { stream: true });

  let newline;
  while ((newline = buffered.indexOf("\n")) !== -1) {
    const line = buffered.slice(0, newline);
    buffered = buffered.slice(newline + 1);
    const seconds = ((performance.now() - started) / 1000).toFixed(2);
    log.textContent += `${line}  +${seconds}s\n`;
  }
}
</script>

保存したHTMLをブラウザーで開きます。まず、最初の1行を読んだ直後に一時停止したのがこんな感じです。

先頭のchunk=1を+0.07秒で受信して読み取りを止めたブラウザー画面

先頭だけを読む記録モードでは、chunk=1が+0.07秒で届き、その時点で読み取りを止めています。レスポンス完了前に先頭チャンクを受け取れたことが分かります。

API GatewayのTestInvokeMethodもストリームを一時的にバッファーします。途中受信の判定には、デプロイ後のURLとストリーム対応クライアントを使います。

続けて、最後まで読み取った記録を確かめます。

4チャンクが約1秒間隔で届き全体が3.10秒で完了したブラウザー画面

通常モードでは4チャンクが+0.09秒、+1.09秒、+2.10秒、+3.10秒に届き、全体は3.10秒でした。約1秒の間隔で行が増えており、Lambdaが書き込んだ順番と一致します。

5. 実際のAPIへ入れる前に確認すること

ストリーミングへ変えると、レスポンスが完成する前にHTTPステータスとヘッダーが確定します。本文の途中で失敗しても、送信済みの200を後から500へ変更できません。SSEならエラーイベント、改行区切りJSONならエラー用レコードを決めるなど、本文内の失敗表現が必要です。

API Gatewayは最初の10MBを帯域制限なしで転送し、それを超えた部分を2MB/秒に制限します。API Gateway側にストリーミング専用の上乗せ料金はありませんが、10MBを超えるレスポンスは10MB単位で複数リクエストとして数えられます。Lambda側は応答ストリームへ書き込んだデータ量が課金対象で、1リクエストの最初の6MBは無料です。月間の無料枠やGBあたりの料金は利用リージョンの料金ページで確認します。

おわりに

ここまでお付き合いいただきありがとうございます。

ニッチといえば、ニッチですが、個人的に実用的だとも思います。

ではまた、お会いしましょう。

参考リンク

AWS公式ドキュメント

AWS公式発表

AWS公式料金

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?