1
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?

インフラ屋が書く Lambda Web Functions:API Gateway+Lambda を置き換えるか、動かして判断する

1
Last updated at Posted at 2026-10-16

変更履歴

  • 2026-10-17:公開
  • 2026-10-17:読みやすさのために構成と説明を見直した

初めに

Lambda に Web Functions(Lambda で普通の HTTP サーバーが動く機能)が来た。最初に浮かんだのは「API Gateway、もう要らないのでは?」という疑問。すぐ次に気になったのは、替えたら運用で何が変わるか。

検証用のアカウントで最小の API を動かし、公式ドキュメントと突き合わせて整理した。アプリ側の用語には脚注を付けた。知っている人は読み飛ばしてほしい。

結論

最小の Express の API で、デプロイから応答までの流れ、同時に来たリクエストの値の取り違え、CloudFront で直接のアクセスを塞ぐ形(4.4)の 3 つを確かめた。デプロイは CLI のコマンド 1 本で通り、値の取り違えは 3 回とも起き、CloudFront を通らない呼び出しは 403 で断れた(認証なしのエンドポイントを残すと、そちらは通る)。

置き換えの候補になる条件は 3 つ。

  • Node.js の HTTP API で、API Gateway に「HTTPS の入口」以上の仕事をさせていない
  • 処理時間の多くが DB や外部 API の応答待ち(I/O 待ち1)
  • 同時に来るリクエストの値を取り違えない書き方になっている(3.4)

逆に、API キーやオーソライザーを使っている API や、Node.js 以外に依存している API は、置き換えが重くなる。同じ関数で S3・SQS などのイベントも受けているなら、HTTP の部分だけを移し、イベントの処理はこれまでの Lambda 関数(Event Functions。1 章)に残す分け方になる。どこで分かれるかは 4.2 の判断フローに書いた。

上の 3 つに当たる Node.js の API なら、Web Functions は API Gateway+Lambda の置き換えの候補になる。

料金と性能、VPC への接続、IaC の対応、CloudWatch のメトリクスは確かめていない(5.5)。

本記事の構成

  1. Web Functions の変更点
  2. 書き方と入口の違い
  3. 動作確認
  4. 選び方
  5. インフラ側の変化

検証の条件は以下。仕様は変わりうるので、日付を基準に読んでほしい。

  • 確認日:2026-10-17
  • リージョン:東京(ap-northeast-1)
  • ランタイム:nodejs24.x(arm64)
  • アプリ:Express2 で書いた最小の API(Express 5.3.0)
  • AWS CLI:aws-cli/2.38.1(lambda-web のコマンドは 2.38.1 で使える。2.34.57 には無い)
  • 作り方:コンソールで作成し、コードの更新は CLI(aws lambda-web deploy)

1. Web Functions の変更点

Lambda の実行環境が「1 件ずつ処理するプロセス」から「普通の Web サーバーのプロセス」になった。これまでの Lambda 関数は、Web Functions と分けて Event Functions と呼ばれるようになった(AWS の開発者ガイドの呼び名。API やリソースの名前は変わらない)。以下、これまでの Lambda を Event Functions と書く。

Apache で言うと、prefork(1 プロセスが同時に 1 接続)から event MPM(1 プロセスが多数の接続)に変わったイメージ。Event Functions は、同時に 10 件来れば実行環境が 10 個できる。Web Functions は、1 つの実行環境が複数のリクエストを同時に抱える。細かい仕組みは違うが、容量の考え方はこれで掴める。

Event Functions Web Functions
コードの形 ハンドラ3 HTTP サーバー(ポートで待ち受けるプロセス)
HTTP の入口 API Gateway や関数 URL が要る 専用の HTTPS エンドポイントが付く
起動の契機 HTTP・S3・SQS・EventBridge など HTTP だけ
1 環境の同時リクエスト 1 件 複数件(上限の設定は既定 64・1〜128)
CPU とメモリ メモリを 128MB〜10GB で選ぶ 1 環境 2 vCPU・2GB で固定
言語とアーキテクチャ 多数・x86 か arm64 Node.js(nodejs24.x)・arm64 だけ
課金の項目 リクエスト数と実行時間(割り当てメモリ×時間) リクエスト数・アクティブな CPU 時間・使ったメモリ・データ転送
IaC のリソース AWS::Lambda::Function AWS::Lambda::WebFunction

1.1 同時リクエストの「64」

64 は上限の設定値。1 環境に実際に何件流すかは、リソースの使われ方を見て Lambda が決める。足りなければ環境を増やし、トラフィックが無くなればゼロまで減る。最低の環境数を確保しておく設定(Event Functions のプロビジョニングされた同時実行に当たるもの)は無い。

1.2 課金

DB の応答待ちのような I/O 待ちの間の CPU 時間は、課金されない(AWS の FAQ より)。CPU 時間は 1 ミリ秒単位、メモリは環境が実際に使った分で数える。

Event Functions は割り当てたメモリ×実行時間なので、待ちが長い API ほど差が出うる。単価はここでは書かない。料金ページで、使うリージョンの値を確かめてほしい。

1.3 リビジョンとエンドポイント

Event Functions のバージョンに当たるのがリビジョン、エイリアスに当たるのがエンドポイント。

リビジョンは、コード・タイムアウト・環境変数・同時実行の上限を固めたもの。変えるたびに新しく作り、50 まで残る。エンドポイントは URL を持ち、どのリビジョンに流すか・認証の種類・流量の上限を持つ。revisionWeights で 2 つのリビジョンに重みを付けられる。新しいリビジョンに一部(例えば 1 割)のリクエストだけを流して様子を見る出し方(カナリアリリース)は、これでそのまま組める。

2. 書き方と入口の違い

コードは読み飛ばしてよい。見てほしいのは入口の形。「Lambda が関数を呼ぶ」から「プロセスがポートで待ち受け、Lambda がそこへ HTTP を流す」に変わる。

2.1 Event Functions の場合

API Gateway(HTTP API)から呼ばれるハンドラを書く。どの URL をどの処理に回すか(ルーティング)は、API Gateway 側に持たせることが多い。

index.mjs
// API Gateway が HTTP リクエストを JSON(event)に変換して渡してくる
export const handler = async (event) => {
  return {
    statusCode: 200,
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ message: 'hello', path: event.rawPath }), // rawPath はリクエストのパス
  };
};

Express を使いたい場合は、アダプター4を挟んでいた。

index.js(アダプターを使う場合)
const serverless = require('serverless-http');
const express = require('express');

const app = express();
app.get('/hello', (req, res) => res.json({ message: 'hello' }));

module.exports.handler = serverless(app); // JSON の event → 偽の HTTP リクエスト → Express → JSON の戻り値

関数の中に、プロトコル変換の中継役が 1 枚入っている形になる。API Gateway 側は全パスを 1 つの関数に流す設定(ANY /{proxy+} のプロキシ統合)にして、振り分けは Express に任せるのが定番だった。見た目は Web サーバーでも、Lambda の呼ばれ方は同じ。1 つの実行環境が同時に扱うのは 1 件のまま。

2.2 Web Functions の場合

普通の Express アプリを書いて、Lambda が環境変数で渡すアドレスで待ち受ける。

index.js
import express from 'express';

const app = express();

app.get('/hello', (req, res) => {
  res.json({ message: 'hello', path: req.path });
});

// Lambda 上では AWS_LAMBDA_HTTP_ENDPOINT に「ホスト:ポート」が入る
const [host, port] = (process.env.AWS_LAMBDA_HTTP_ENDPOINT || 'localhost:3000').split(':');
app.listen(Number(port), host);
package.json
{
  "name": "hello-web",
  "type": "module",
  "dependencies": {
    "express": "^5.0.0"
  }
}

package.json は依存するライブラリを書く設定ファイル。npm install を打つと、書いたライブラリが node_modules フォルダに入る(apt install をアプリのフォルダの中だけでやるイメージ)。Lambda に渡すときは、この node_modules ごと ZIP にし、ライブラリも一緒に配る。"type": "module" は、上の import の書き方を使うときに付ける宣言(require で書くなら要らない)。

書き換えで気をつけるのは次の 4 つ。どれも起動の確認(ヘルスチェック)に通るかの話。

  • 入口はパッケージ直下の index.js(別のファイルにするなら環境変数 AWS_LAMBDA_NODEJS_ENTRYPOINT で指定)
  • listen(3000) や localhost 固定では通らない。AWS_LAMBDA_HTTP_ENDPOINT を読む
  • アダプターは外す。残すとポートを開かず、起動の確認に通らない
  • 起動してから 10 秒以内に待ち受けを始める

2.3 API Gateway がしていた仕事の行き先

API Gateway で使っていた機能 Web Functions での受け皿
HTTPS の入口 エンドポイントが付く
IAM 認証 エンドポイントの認証を IamAuth(AWS の署名付きの呼び出しだけを通す設定。3.1)にする
JWT・Cognito・Lambda オーソライザー(ログインしたユーザーの確認) アプリで実装する
流量制御(スロットリング) エンドポイントの rateLimit(毎秒のリクエスト数。超えると 429)。クライアントごとの制御ではない
API キー・使用量プラン(キーごとの上限) 相当するものは無い。アプリで実装する
リクエストの検証(必須項目や JSON の形の確認) アプリで実装する
独自ドメイン・WAF エンドポイントには直接付かない。CloudFront を前に置く

「HTTPS の入口」以外を使っている行が多いほど、置き換えは重くなる。

3. 動作確認

項目 内容
目的 作成からデプロイ・応答・ログまでを一通り動かし、運用で気にする所(作成時に決めること・デプロイの確かめ方・ログの置き場)を見る
背景 入口と実行環境の形が変わった。Event Functions の感覚のままだと、作成後に変えられない設定や、ログの置き場で迷う
やったこと コンソールで Hello World の関数を作り(3.1)、2 章のコードを CLI でデプロイして curl で呼び(3.2)、CloudWatch Logs を見た(3.3)。3.4 で値の取り違えを試した

3.1 コンソールで作る

Lambda のコンソールの左メニューに Web Functions が増えている。

図1 Web Functions の一覧

「Create Web Function」(日本語の画面では「ウェブ関数を作成」)から作る。今回はコードの元を Hello World のテンプレートにして、あとで自分のコードに差し替えた。

図2 作成画面の基本情報

同じ画面でエンドポイントも作る。決めるのは 3 つ。

1 つ目は種類。後から変えられない。

  • HomeRegion:1 つのリージョンで動かす。URL もそのリージョンのものが 1 つ
  • MultiRegion:選んだ複数のリージョンで動かし、URL は 1 つ。呼び出し元から近いリージョンへ Lambda が振り分ける
  • PerRegion:選んだ複数のリージョンで動かし、URL はリージョンごとに分かれる。振り分けは自分で組む(Route 53 や CloudFront)

迷ったら HomeRegion。ドキュメントも、まず始めるなら HomeRegion を挙げている。下の自動デプロイと、VPC への接続(5.2)が使えるのも HomeRegion だけ。

2 つ目は認証。ApplicationManaged は、Lambda が認証せず、全部のリクエストをアプリに渡す(認証はアプリで書く)。IamAuth は、SigV4 で署名した呼び出しだけを Lambda が通す。SigV4 は、AWS の認証情報でリクエストに署名する方式で、AWS CLI や SDK が AWS の API を呼ぶときと同じもの。通るのは lambda:InvokeWebFunctionEndpoint の権限を持つ相手だけになる。今回は動作確認なので ApplicationManaged にした。

3 つ目は自動デプロイ(Auto deployment)。Latest revision にすると、新しいリビジョンが Active になったとき、エンドポイントの向き先を Lambda が切り替える。Disabled なら、向き先は自分で重みを付けて変える(ドキュメントは本番向きにこちらを勧めている)。コンソールの既定は Latest revision で、今回もそのままにした。

図3 エンドポイントの設定

作成すると、リビジョンとエンドポイントが Pending(準備中)から Active(呼び出しを受けられる状態)に変わる。今回は、作成を押してから関数が 10 秒ほど、エンドポイントが 30 秒ほどで Active になった。実行ロール・コードを置く S3 バケット・ロググループも、このとき一緒に作られる。

図4 リビジョンが Active になった

関数の概要の画面に、エンドポイントと呼び出す URL が出る。

図5 エンドポイントの URL

3.2 自分のコードに差し替える

2 章の Express アプリを CLI でデプロイした。コンソールで作った関数にも、aws lambda-web deploy がそのまま通った。

このコマンドは、ZIP の作成・S3 への配置・リビジョンの作成をまとめて行う。コードは S3 に置いた ZIP からしか渡せない。新しいリビジョンは前の設定(環境変数など)を引き継がない。

aws lambda-web deploy --name hello-web --code ./hello-web --region ap-northeast-1

結果は以下(待ちの途中経過の行を省き、ドメインは伏せた)。

Creating deployment package ...
Warning: skipped 58 hidden paths: node_modules/.package-lock.json, node_modules/call-bind-apply-helpers/.github, ... and 53 more. Use --include-hidden-files to include hidden files and directories that don't match an exclusion pattern.
Preparing Amazon S3 bucket ...
Uploading deployment package ...
Creating Web Function revision ...
Waiting for revision rev-0jqpt to become active (timeout 600s) ...
Endpoint 'dev' auto-deploys the latest revision and will serve the new revision after it becomes Active.
Waiting for endpoint 'dev' to become active (timeout 600s) ...
https://<エンドポイントのドメイン>

Warning の行は、. で始まる隠しファイルを ZIP から外したという知らせ。動かすのに要るなら --include-hidden-files を付ける。

自動デプロイが Latest revision なので、新しいリビジョンが Active になると、向き先も切り替わる。

図6 2 つ目のリビジョン

curl で叩くと、2 章のとおりの JSON が返った。

curl -s https://<エンドポイントのドメイン>/hello
{"message":"hello","path":"/hello"}

ドキュメントによると、待ち受けに失敗したリビジョンも Active になる(今回は試していない)。ステータスだけ見ても、動いているかは分からない。なので、デプロイのたびに URL を叩く。パイプラインのスモークテストにしておく。

3.3 ログの場所と中身

ログは CloudWatch Logs の /aws/lambda/web/<関数名> に出る。Event Functions の /aws/lambda/<関数名> とは別の名前になる。

もう 1 つ、/aws/lambda/web/<関数名>.local もできていた。どちらも保持期間は「失効しない」になっていた。

ドキュメントによると、.local はリクエストを処理したリージョンごとの置き場。ログはまず .local に書かれ、あとで .local の無いほうへまとめられる。まとめたあとも .local からは消えない。なので、保持期間は 2 つとも設定する。

図7 ログの中身

中身は 1 行が 1 つの JSON。起動(platform.initStart)、アプリの出力(今回は listening on 0.0.0.0:3000 の 1 行だけ)、1 分ごとの集計(platform.invokeSummary)が並ぶ。集計はリクエストが無い 1 分にも 0 で出る。リクエストごとの行(アクセスログ)は、既定のログの段階(INFO)では出ない。集計の 1 行は以下。

{"timestamp":"2026-10-16T16:17:13.346Z","level":"INFO","type":"platform.invokeSummary","region":"ap-northeast-1","record":{"startTime":"2026-10-16T16:16:13.348308279Z","instanceId":"33ddf31d-…","metrics":{"requests":4,"concurrentRequests":2,"cpuTimeMs":77.000,"maxMemoryUsageMb":168}}}

metrics の値は、どれも startTime からの約 1 分の分。上の行は、この 1 分に 4 件を処理し終え、同時には最大 2 件を抱え、CPU を 77 ミリ秒使ったという意味。

  • requests:この 1 分に終わったリクエストの数
  • concurrentRequests:この 1 分に同時に処理した数の最大
  • cpuTimeMs:この 1 分に使った CPU 時間(ミリ秒)
  • maxMemoryUsageMb:実行環境が使ったメモリの最大(MiB)

instanceId が実行環境ごとに違い、ログストリームも環境ごとに分かれる。アクセスログが要るなら、アプリで書くか、前段(CloudFront など)のログを使う。環境が何分で消えるかは、ドキュメントに書かれていない。

ロググループ名を前提にした設定(メトリクスフィルター・アラーム・サブスクリプションフィルターなど)は、移すときに付け替える(今回は作っていない)。

3.4 リクエストの値の取り違え

項目 内容
目的 同時に来たリクエストの値が、Web Functions で実際に混ざるかを確かめる
背景 Event Functions は 1 環境で 1 件ずつ処理するので、リクエストごとの値をモジュールのトップレベルの変数5に置いても表に出にくかった。Web Functions は 1 つのプロセスが複数件を同時に処理するので、同じ書き方だと他のリクエストの値が返る
やったこと わざと悪い書き方のパスを足し、名前だけ違う 4 本を続けて投げた。手元で 1 回、Web Functions で 3 回

症状は、CDN のキャッシュ設定のミスで他人のページが返る事故と同じ。混ざる値が利用者の情報なら、情報漏えいになりうる。

Node.js の Web サーバーなら普通に起きること。Lambda に慣れた書き方のまま移すと踏みやすい。

足したのは、名前をクエリで受け取り、0.3 秒待って(DB の応答待ちのつもり)、その名前を返すだけのパス。

index.js(追記)
let currentName; // リクエストごとの値を、プロセス全体で共有される変数に置いている(悪い例)

app.get('/whoami', async (req, res) => {
  currentName = req.query.name;
  await new Promise((r) => setTimeout(r, 300)); // 0.3 秒待つ。この間にほかのリクエストが currentName を上書きできる
  res.json({ asked: req.query.name, answered: currentName });
});

手元(Node.js 24・プロセス 1 つ)では次のように投げた。末尾の & で curl を裏で走らせるので、1 本目の応答(0.3 秒後)を待たずに次が出ていく。

for n in alice bob carol dave; do
  curl -s "http://localhost:3000/whoami?name=$n" &
done
wait

結果は以下。

{"asked":"alice","answered":"dave"}
{"asked":"bob","answered":"dave"}
{"asked":"carol","answered":"dave"}
{"asked":"dave","answered":"dave"}

4 本中 3 本に、dave の名前が返った。alice の 0.3 秒の待ちが終わる前に、bob・carol・dave が届いて変数を順に上書きしたため。dave が正しく見えるのは、たまたま最後に書いたから。混ざる条件は「同時に送ること」ではなく、待ちの間に別のリクエストが同じプロセスに届くこと。待ちが長いほど重なりやすい。

同じことを Web Functions のエンドポイントに投げた。

for n in alice bob carol dave; do
  curl -s "https://<エンドポイントのドメイン>/whoami?name=$n" &
done
wait

3 回投げた。結果は以下(届いた順のまま、1 本ずつ改行した)。

1 回目
{"asked":"bob","answered":"bob"}
{"asked":"alice","answered":"bob"}
{"asked":"carol","answered":"dave"}
{"asked":"dave","answered":"dave"}

2 回目
{"asked":"bob","answered":"dave"}
{"asked":"alice","answered":"alice"}
{"asked":"carol","answered":"carol"}
{"asked":"dave","answered":"dave"}

3 回目
{"asked":"alice","answered":"dave"}
{"asked":"bob","answered":"bob"}
{"asked":"carol","answered":"carol"}
{"asked":"dave","answered":"dave"}

3 回とも、asked と answered がずれた行がある。ずれたのは 4 本中 2 本・1 本・1 本。手元では 4 本中 3 本だった。手元より少なく、回によっても違う。

ログの 1 分ごとの集計(3.3)を環境ごとに足すと、1 回目の 4 本は 1 つの環境に届き、2・3 回目の 8 本は 3 つの環境に分かれて届いていた。ずれが手元より少ないのは、別の環境に届いた分が混ざらなかったためと読める。1 本ずつの行き先はログに残らないので、ここは推定になる。

環境の数は、1 回目を投げたのと同じ 1 分のうちに 1 つから 3 つになった(platform.initStart の行が 2 つ増えた)。その 1 分に 1 つの環境が同時に処理したのは、最大 2 件(concurrentRequests)。上限の 64 には届いていない。1.1 のとおり 64 は上限にすぎず、1 環境に何件まで載せるかは Lambda が決める。

別の実行環境に振り分けられれば混ざらないので、毎回は再現しない。なので、負荷試験で出なくても安心はできない。直し方は、値を関数の中のローカル変数か、Express がリクエストのたびに作る req・res に持たせること。リクエストをまたいで値を受け渡したいなら AsyncLocalStorage6 を使う。

1 つの環境で同時に処理することで起きる問題は、もう 1 つある(今回は試していない)。CPU を長く使う処理(大きな JSON の変換、画像処理など)は、同じ実行環境の他のリクエストを待たせる。Node.js は JavaScript の処理を 1 本のスレッドで回すため1。2 vCPU あっても、アプリの処理に使えるのは実質 1 つと見ておくのが安全。両方使うなら worker_threads などで処理を分ける。

4. 選び方

4.1 比較

Event Functions と Web Functions の違いは 1 章の表のとおり。ここでは ECS Express Mode(コンテナイメージを渡すと、ALB などの前段まで ECS が作って公開する仕組み)を、Web Functions の条件に合わないときの分岐として足した。並べたのは分かれ目になる行だけ(ECS の列はドキュメントだけで書いている)。

Event Functions Web Functions ECS Express Mode
HTTP の入口 API Gateway・関数 URL など 専用の HTTPS エンドポイント ALB などを自動で作る
HTTP 以外のイベント 受けられる 受けられない 受けられない(常駐のサービス)
配布の形 コード・コンテナイメージ コード コンテナイメージ
向く先 イベント処理・既存の API Node.js の HTTP API・画面向けの中継 API コンテナ化した API・Web サービス

4.2 判断フロー

終わりを「候補として試す」にしたのは、料金と性能はワークロードで測らないと分からないから。

4.3 移す価値の見積もり方

手元の数字で見積もるなら、見るのは次の 4 つ。

  • 実行時間のうち、DB や外部 API を待っている割合(X-Ray などのトレース)。待ちが長いほど、CPU 時間で数える Web Functions が有利になりうる
  • 同時実行数のピークと平均(CloudWatch の ConcurrentExecutions)。同時に来るほど、1 つの環境にまとめて載せられる
  • API Gateway の料金(Cost Explorer)。外せる分は減る。ただし CloudFront・WAF を足すなら、その分が増える
  • 移す手間。アダプターの撤去(2.2)、変数の見直し(3.4)、IaC のリソースの書き換え(1 章の表)、ロググループ名を前提にしたアラームやフィルターの付け替え(3.3)

単価は料金ページで確かめる。小さな API を 1 本だけ試しに移して、1 か月の請求とレイテンシを比べるのがいちばん確実。

4.4 構成の例

ケース A:API Gateway に HTTPS の入口だけを任せている場合。Client → API Gateway → Event Function → DB が、Client → Web Function のエンドポイント → DB になる。

いちばん素直に移せる形。認証をアプリでしているなら、そのまま持っていける。DB が VPC の中にある場合の図は以下(ドキュメントをもとに描いた。仕組みは 5.2)。

図A ケース A の変更前と変更後

ケース B:独自ドメインや WAF が要る場合。

Client → CloudFront(+WAF・独自ドメイン)→ Web Function のエンドポイント

図B ケース B の構成

ケース B は実際に組んで試した。

項目 内容
目的 CloudFront を前に置いたとき、エンドポイントへの直接のアクセスを AWS の設定で塞げるかを確かめる
背景 エンドポイントは認証の種類にかかわらずインターネットから届き、VPC の中に隠せない。WAF や独自ドメインを CloudFront に付けても、URL を知っていれば CloudFront を通らずに呼べる
やったこと 認証を IamAuth にしたエンドポイントを足し、CloudFront(OAC 付き)とリソースポリシーを組んで、CloudFront 経由と直接の呼び出しを比べた

AWS の側で塞げるのは、次の組み合わせだけ。

  • エンドポイントの認証を IamAuth にする
  • CloudFront のオリジンアクセスコントロール(OAC)で、オリジンへのリクエストに CloudFront が SigV4 で署名する
  • リソースポリシーで、特定のディストリビューションからの呼び出しだけを許す

ただし、塞げるのは「署名の無い呼び出し」。IAM の権限を持つ人(同じアカウントの管理者など)が署名すれば、直接呼べる。また、CloudFront からは Host ヘッダーを転送しない(転送すると 404)。

組み方は以下。

  1. 同じ関数に、認証を IamAuth にしたエンドポイントを 1 つ足す
  2. CloudFront のディストリビューションを作る。オリジンの種類で Lambda function を選び、1 のドメインを入れる。「Allow private Lambda function access to CloudFront」に印を付けると、OAC が自動で作られる
  3. キャッシュポリシーは CachingDisabled、オリジンリクエストポリシーは AllViewerExceptHostHeader にする
  4. Web Function のリソースポリシーで、CloudFront から 1 のエンドポイントを呼ぶことを許す。コンソールの「リソースベースのポリシー」タブから JSON で書けた

リソースポリシーは以下。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontOACAccess",
      "Effect": "Allow",
      "Principal": { "Service": "cloudfront.amazonaws.com" },
      "Action": "lambda:InvokeWebFunctionEndpoint",
      "Resource": "arn:aws:lambda:ap-northeast-1:<アカウント ID>:web-function/hello-web/endpoint/<エンドポイント名>",
      "Condition": { "StringEquals": { "aws:SourceArn": "arn:aws:cloudfront::<アカウント ID>:distribution/<ディストリビューション ID>" } }
    }
  ]
}

OAC は、手順 2 のとおりディストリビューションの作成画面に作らせる。作られる OAC の署名動作は always-amz-auth(画面の表記は「Always sign, preserve viewer authorization」)。OAC を手で作るなら、署名動作をこれにする。オリジンの種類で Lambda を選んだときの既定は always(「常にリクエストに署名する」)で、その OAC をオリジンに付けると、保存の時点で次のエラーになる。

Illegal configuration: Lambda-Web origins require always-amz-auth signing behavior.

図8 CloudFront のオリジンの設定

図9 リソースポリシー

CloudFront 経由と、エンドポイントへの直接の呼び出しを比べる。

# CloudFront 経由
curl -s -w " %{http_code}\n" https://<ディストリビューションのドメイン>/hello
# エンドポイントへ直接(署名なし)
curl -s -w " %{http_code}\n" https://<エンドポイントのドメイン>/hello

結果は以下(Request ID は伏せた)。

{"message":"hello","path":"/hello"} 200
<!DOCTYPE html>
<html>
<head>
    <title>HTTP Error 403</title>
</head>
<body>
    <h1>403 Forbidden</h1>
    <p>You are not authorized to access this endpoint.</p>
    <p>Request ID: <伏せた></p>
</body>
</html> 403

CloudFront 経由は 200、直接は 403。直接のほうの本文は JSON ではなく HTML のエラーページだった。API の呼び出し側が JSON の応答を前提にしていると、ここで読み違える。

認証はエンドポイントごとに分かれる。同じ関数にある ApplicationManaged のエンドポイント(3.1 で作った dev)は、同じときに直接呼んでも 200 が返った。迂回路を塞ぐなら、認証なしのエンドポイントを置かない。

認証を ApplicationManaged のままにする場合や、API Gateway など CloudFront 以外を前段に置く場合は、直接届く状態が残る。その場合は、前段が付ける秘密のヘッダーやトークンをアプリで確かめて、無いリクエストを断る。

ケース C:コンテナで動かしている場合(Client → ALB → ECS/Fargate)。Web Functions には載らないので、比べる相手は ECS Express Mode。最小タスク数の既定は 1 なので、ゼロまで縮む Web Functions とは待機中の費用のかかり方が違う(0 にできるかは未確認)。App Runner は 2026 年 4 月 30 日から新規の受け付けを止めている。

5. インフラ側の変化

5.1 運用の差分

観点 API Gateway+Event Functions Web Functions
入口の公開範囲 プライベート API にもできる 常にインターネットから届く(PrivateLink も不可)
VPC 内への接続 関数を VPC に置く ネットワークコネクタ(VPC への出口になる Lambda のリソース。5.2)経由
プロトコル API Gateway の仕様しだい HTTP/1.1(HTTP/2・3 は CloudFront の手前で)
ログ /aws/lambda/<名前> /aws/lambda/web/<名前> と .local(保持期間は両方に)
棚卸し aws lambda list-functions aws lambda-web list-web-functions
流量と規模の上限 予約同時実行・スロットリング エンドポイントの rateLimit(毎秒のリクエスト数)と maxEnvironments(実行環境の数の上限。既定は無し)

IamAuth は呼べる人を絞るだけで、ネットワークとして閉じるわけではない。なので、直接の呼び出しをどう扱うかは、4.4 のケース B で決めておく。

5.2 VPC まわり

ドキュメントによると、次のように変わる。

関数そのものは Lambda 側の基盤で動き、VPC には入らない。VPC へ出るには、ネットワークコネクタを使う。コネクタは関数とは別に 1 度作っておき、各リビジョンから参照する。リビジョンが出口用のコネクタを参照すると、外向きの通信だけが、コネクタの ENI(VPC のサブネットに置かれる仮想のネットワークインターフェース)を通って VPC に入る。使えるのは HomeRegion のエンドポイントだけ。ENI は別々の AZ の 2 つ以上のサブネットに置く。コネクタには、ENI を作らせるためのオペレーターロールが、実行ロールとは別に要る。

宛先側では、コネクタのセキュリティグループを許可する。送信元 IP は固定されない。SG で参照できない宛先なら、コネクタのサブネットの CIDR を許可する。

インターネットへの外向きも変わる。コネクタ無しなら直接出られるが、送信元 IP は Lambda 側のもので、許可リストには使えない。コネクタを付けると経路が置き換わるので、NAT ゲートウェイへのルートが要る。許可リストで絞っている外部 API があるなら、コネクタ+NAT の構成になる。

5.3 規模の上限

リージョンごとに、1 エンドポイントは毎秒 10,000 件、アカウント全体は毎秒 100,000 件と 2,000 vCPU まで。1 環境が 2 vCPU を使うので、同時に動く環境はリージョン全体で 1,000 が上限。同じアカウントに Web Functions が何本もあると、この枠を取り合う。上限は引き上げを申請できる。新しいアカウントでは vCPU と毎秒のリクエスト数の上限が低めに始まり、使うにつれて自動で上がる。

5.4 移す前のチェックリスト

インフラ側でやること。

  • 5.1 の表の右列に合わせて、入口・ログ・棚卸し・上限を直した。ビルドの環境を arm64 にした(x86 でビルドしたネイティブモジュール7は動かない)
  • スモークテスト(3.2)をパイプラインに入れた
  • アクセスログをどこで取るか決めた(Lambda は既定では出さない。3.3)
  • 秘密を環境変数に置かず、Secrets Manager か SSM から読む(リビジョンの設定を読める人には環境変数が見える)

アプリ担当に確かめるのは、2 章の書き換えの 4 点、3.4 の取り違え、2.3 でアプリに移る仕事の 3 つ。ほかに、1 リクエストにかかる時間。タイムアウトの既定は 30 秒で、3〜900 秒の間で設定する。ブラウザから呼ぶなら、CORS8 の応答をアプリか前段で返すこと(Lambda は付けない)。Node.js の細かい版は AWS が自動で上げるので、固定できない。

5.5 確かめていないこと

  • 料金:Web Functions の単価と無料枠
  • 性能:コールドスタート・レイテンシ
  • VPC への接続:ネットワークコネクタを使った接続
  • IaC:Terraform・CDK の対応状況(CloudFormation のリソースがあることだけ確かめた)
  • CloudWatch のメトリクスの名前と、アラームに使える指標

最後に

構成が 1 段減る代わりに、API Gateway が持っていた仕事が別の場所へ散る。ログインの確認・キーごとの上限・リクエストの検証はアプリへ(2.3)、独自ドメインと WAF は CloudFront へ移る。直接のアクセスを塞ぐ所も分かれる(4.4 のケース B)。AWS の設定で組むなら、エンドポイントの認証・OAC・リソースポリシー・CloudFront の 4 か所。組まないなら、アプリのコードになる。試した範囲でも、OAC の署名動作を違えると保存できず、認証なしのエンドポイントを残すと迂回路が残った。

Web Functions は「Lambda で普通の Web サーバーが動く」ようになったもので、API Gateway の機能一式の代わりにはならない。結論の 3 つに当たる API でも、散った仕事をどこで持つかをアプリ担当と取り決めてから移す。

次は、4.3 の見積もり方で、仕事で触っている小さな API を 1 本移して確かめる。

参考

確認日は 2026-10-15。上限・ランタイム・ベストプラクティス・ログ・実行環境のライフサイクル・What's New の 6 本は 2026-10-17 に見直した。

  1. I/O 待ちと Node.js の処理モデル:I/O 待ちは、DB・外部 API・ファイルの応答を待っている時間で、CPU はほぼ使わない。Node.js はリクエストを 1 本のスレッドで処理し、待ちの間に別のリクエストを進める(イベントループ)。なので 1 プロセスで多数のリクエストを抱えられる。ただし、CPU を使い続ける処理があると、その間はほかのリクエストが全部止まる。 ↩ ↩2

  2. Express:Node.js で Web サーバーを書くときの定番のフレームワーク(npm で配られている部品)。Node.js だけでも HTTP サーバーは立つが、URL やメソッドの振り分けを全部自分で書くことになる。Express は、URL ごとに処理を割り当てる(ルーティング。nginx の location で振り分けるのを、アプリの中でやるイメージ)。リクエストの前後に共通の処理(ログ・認証など)を挟むミドルウェアの仕組みも持つ。Web Functions では、HTTP サーバーを立てる Node.js のフレームワークなら Express 以外も使える(Fastify・Koa・Next.js など)。 ↩

  3. ハンドラ:Lambda から呼ばれる関数。イベント(API Gateway が HTTP リクエストを JSON にしたものなど)が引数で渡され、戻り値が応答になる。 ↩

  4. アダプター:Lambda のイベントを HTTP リクエストの形に変換して Express に渡すライブラリ(serverless-http・serverless-express など)。Web Functions では Lambda が HTTP をそのまま流すので要らない。 ↩

  5. モジュールのトップレベルの変数:ファイルのいちばん外側で宣言した変数。プロセスが生きている間ずっと残り、そのプロセスが処理する全リクエストから見える。ほかの言語でいうグローバル変数に近い。 ↩

  6. AsyncLocalStorage:Node.js 標準の機能。1 件のリクエストの処理の流れに値をひも付け、ほかのリクエストから見えないようにする。ログにリクエスト ID を付けるときなどに使う。 ↩

  7. ネイティブモジュール:C/C++ で書かれ、インストールのときに CPU アーキテクチャ向けにビルドされるライブラリ。x86 でビルドしたものは arm64 では動かない。画像処理の sharp、パスワードのハッシュの bcrypt など。 ↩

  8. CORS:ブラウザが、別のドメインの API を呼んでよいかを確かめる仕組み。API 側が許可のヘッダーを返さないと、ブラウザは応答を捨てる。 ↩

1
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
1
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?