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?

API Gateway + Lambda 構成の正体——なぜ Gateway を挟むのか、リクエストは何を通るのか、どこで詰まるのか

0
Posted at

はじめに

EC2 に nginx を入れて HTTP を受けるなら、話は単純だ。サーバーを立てて listen 80 と書けば、そこにリクエストが届く。

server {
    listen 80;
    server_name api.example.com;
}

では Lambda は? 立てるサーバーがない。listen を書く場所もない。

それなのに https://xxxx.execute-api.ap-northeast-1.amazonaws.com/prod/users を叩くと Lambda が動く。何が HTTP を受けているのか。

「API Gateway + Lambda」はサーバーレスの定番構成として名前だけはよく出るが、中で何が起きているかは案外説明されない。この記事では次の5つに答える。

  • なぜ API Gateway を挟むのか(挟まない選択肢はないのか)
  • リクエストは何を通っているのか(Lambda は何を受け取るのか)
  • REST API と HTTP API のどちらを選ぶのか(名前が紛らわしい2種類がある)
  • どこで詰まるのか(設計前に知っておくべき制限値)
  • ログはどこに出て、VPC にはいつ入れるべきか(3 か所に分かれるログの出どころと、VPC 接続が持ち込むトレードオフ)

👉 API Gateway には WebSocket API という3つ目の種別もあるが、この記事では扱わない。REST API と HTTP API の2つに絞る。

👉 この記事に出てくる制限値・仕様・料金はすべて 2026-08-11 時点の AWS 公式ドキュメントに基づく。記事末尾に出典を挙げた。


なぜ API Gateway を挟むのか

Lambda には HTTP のリスナーがないからだ。

Lambda の呼び出し口は Invoke API ひとつしかない。AWS の API を叩いて「この関数を、この JSON を渡して実行しろ」と指示する仕組みであって、TCP の 80 番で待ち受ける仕組みではない。

つまり、Lambda を HTTP で叩けるようにするには、

HTTP リクエストを受け取って、Invoke の呼び出しに翻訳する誰か

が必ず要る。API Gateway はその翻訳機だ。

👉 そして翻訳のついでに、認証・スロットリング・リクエスト検証・レスポンス変換を引き受けている。API Gateway の価値は翻訳そのものより、この「ついで」の部分にある。


翻訳機は API Gateway だけではない

HTTP を Lambda に届ける手段は3つある。

Function URL API Gateway ALB のターゲット
認証 IAM または無し(2択のみ) IAM / Lambda オーソライザー / Cognito / JWT 本記事では扱わない
カスタムドメイン 不可 可 可
スロットリング 予約された同時実行のみ API / ステージ / メソッド単位 —
リクエスト検証・変換 なし あり(REST API) なし
リクエストボディ上限 6 MB(Lambda の上限) 6 MB(後述) 1 MB
レスポンス上限 6 MB(Lambda の上限) 6 MB(後述) 1 MB
VPC 内への公開 不可(PrivateLink 非対応) REST API のプライベートエンドポイントで可 可
WebSocket — WebSocket API で可 不可(Upgrade は 400 で拒否)
追加課金 なし(Lambda の課金のみ) API Gateway 分が加算 ALB 分が加算

❌ 「Lambda を HTTP で公開するなら API Gateway しかない」は誤解。

  • Function URL は関数に専用の HTTPS エンドポイント(https://<url-id>.lambda-url.<region>.on.aws)を1つ生やすだけの機能だ。追加課金はない
  • ALB のターゲットとして Lambda を登録することもできる。ALB はネットワーク接続ではなく直接 Lambda を呼び出すので、ALB 側のセキュリティグループにアウトバウンドルールを書く必要すらない

👉 AWS 自身のドキュメントも「シンプルなアプリケーションやプロトタイピングで、基本的な認証とリクエスト/レスポンス処理だけが必要、かつコストと複雑さを最小に抑えたい場合は Function URL」を推奨している。「とりあえず API Gateway」は必ずしも正解ではない。

では API Gateway を選ぶ理由は何か。HTTP を通すためではない。

  • IAM 以外の認証方式(Lambda オーソライザー、Cognito、JWT)を使いたい
  • カスタムドメインを当てたい
  • API / ステージ / メソッド単位でスロットリングしたい
  • リクエストを検証・変換したい
  • VPC 内だけに公開したい

この5つのどれかに当てはまるかどうかが分かれ目になる。


リクエストは何を通るのか

経路はこうなっている。

クライアント
  │ HTTPS
  ▼
API Gateway
  │  ① ルート照合(どのメソッド / パスか)
  │  ② オーソライザー実行(認可)
  │  ③ 統合(HTTP リクエストを JSON イベントに変換)
  │ Invoke(JSON イベント)
  ▼
Lambda 実行環境
  │  Init(初回のみ)→ Invoke(ハンドラ実行)
  │ JSON レスポンス
  ▼
API Gateway
  │  ④ JSON を HTTP レスポンスに再構成
  ▼
クライアント

ポイントは ③ と ④ で JSON をまたいでいることだ。Lambda は HTTP リクエストを受け取らない。受け取るのは HTTP リクエストを JSON に書き写したものだ。

👉 統合には「プロキシ統合」と「非プロキシ統合」がある。この記事はプロキシ統合(リクエストをまるごと Lambda に渡し、Lambda の返り値をそのままレスポンスにする方式)を前提にする。実際の構成はほぼこちらだ。


Lambda が受け取るイベント

プロキシ統合にはペイロード形式のバージョンが 1.0 と 2.0 の2つある。

  • REST API は 1.0 のみ
  • HTTP API は 1.0 と 2.0 を選べる

これが後で効いてくるので、両方見ておく。

1.0(REST API / HTTP API)

{
  "version": "1.0",
  "resource": "/my/path",
  "path": "/my/path",
  "httpMethod": "GET",
  "headers": { "header1": "value1" },
  "multiValueHeaders": { "header2": ["value1", "value2"] },
  "queryStringParameters": { "parameter1": "value1" },
  "multiValueQueryStringParameters": { "parameter1": ["value1", "value2"] },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "id",
    "authorizer": { "claims": null, "scopes": null },
    "domainName": "id.execute-api.us-east-1.amazonaws.com",
    "httpMethod": "GET",
    "identity": { "sourceIp": "192.0.2.1", "userAgent": "user-agent" },
    "path": "/my/path",
    "protocol": "HTTP/1.1",
    "requestId": "id=",
    "stage": "$default"
  },
  "pathParameters": null,
  "stageVariables": null,
  "body": "Hello from Lambda!",
  "isBase64Encoded": false
}

👉 ただし REST API のイベントには version フィールドが無い。HTTP API のペイロード形式 1.0 には "version": "1.0" が含まれる。「1.0 形式」と呼ばれていても、REST API と HTTP API のイベント構造は完全に同一ではない。

2.0(HTTP API のみ)

{
  "version": "2.0",
  "routeKey": "$default",
  "rawPath": "/my/path",
  "rawQueryString": "parameter1=value1&parameter1=value2",
  "cookies": ["cookie1", "cookie2"],
  "headers": { "header1": "value1", "header2": "value1,value2" },
  "queryStringParameters": { "parameter1": "value1,value2" },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "api-id",
    "authorizer": { "jwt": { "claims": {}, "scopes": [] } },
    "domainName": "id.execute-api.us-east-1.amazonaws.com",
    "http": {
      "method": "POST",
      "path": "/my/path",
      "protocol": "HTTP/1.1",
      "sourceIp": "192.0.2.1",
      "userAgent": "agent"
    },
    "requestId": "id",
    "routeKey": "$default",
    "stage": "$default",
    "time": "12/Mar/2020:19:03:58 +0000",
    "timeEpoch": 1583348638390
  },
  "body": "Hello from Lambda",
  "pathParameters": { "parameter1": "value1" },
  "isBase64Encoded": false
}

違いは3つだけ。

1.0 2.0
複数値のヘッダー / クエリ multiValueHeaders / multiValueQueryStringParameters に配列で入る フィールドが無い。カンマ結合されて headers / queryStringParameters に入る
生のパス なし rawPath がある
Cookie headers の中 cookies 配列に分離される。レスポンスでは各要素が set-cookie ヘッダーになる

👉 2.0 ではヘッダー名がすべて小文字化される。

👉 rawPath にはカスタムドメインの API マッピング値が出ない。マッピング込みのパスが欲しいなら 1.0 の path を使う。

👉 コンソールで統合を作るとデフォルトで最新版(2.0)になるが、CLI / CloudFormation / SDK で作る場合は payloadFormatVersion の指定が必須だ。IaC に書き忘れるとエラーになる。


Lambda が返すべきレスポンス

プロキシ統合では、Lambda の返り値がそのまま HTTP レスポンスになる。だから決まった形の JSON を返さなければならない。

export const handler = async (event) => {
  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ message: "ok" }),
  };
};

statusCode / headers / body / isBase64Encoded(と 1.0 の multiValueHeaders、2.0 の cookies)がフィールドの全部だ。

❌ 「関数は正常終了しているのに 502 Bad Gateway が返る」——これはこの構成で最も多いつまずきだ。

関数の出力がこの形式と異なると、API Gateway は 502 Bad Gateway を返す。

典型的な原因は2つ。

// ❌ オブジェクトをそのまま返している(statusCode が無い)
return { message: "ok" };

// ❌ body が文字列になっていない
return { statusCode: 200, body: { message: "ok" } };

Lambda 側のログを見ると「正常終了」と出ているので、原因が Lambda ではなく API Gateway 側の形式チェックにあることに気づきにくい。

👉 ただし HTTP API の payload format 2.0 だけは例外だ。

2.0 では、関数が有効な JSON を返して statusCode を含まない場合、API Gateway が次のように推論する。

  • statusCode = 200
  • content-type = application/json
  • isBase64Encoded = false
  • body = 関数のレスポンスそのもの

つまり return { message: "ok" }; は、2.0 では 200 OK で {"message":"ok"} が返る。

👉 同じハンドラのコードが、REST API では 502、HTTP API(2.0)では 200 になる。 REST API から HTTP API へ移行するとき、あるいはその逆のときに、この違いが事故になる。

👉 CORS を有効にするには、REST API では出力の headers に Access-Control-Allow-Origin を自分で追加する必要がある。HTTP API には組み込みの CORS 設定があるため、この手当ては要らない。


オーソライザーはどこで走るのか

統合の手前だ。認可に失敗すれば Lambda は呼ばれず、その分の課金も発生しない。

認可の結果は requestContext に載って関数まで届く。何が届くかは認可方式で変わる。

認可方式 関数に届くもの
AWS_IAM $context.identity.*
COGNITO_USER_POOLS $context.identity.cognito* と $context.authorizer.claims.*
CUSTOM(Lambda オーソライザー) $context.authorizer.principalId ほか

👉 認可を有効にしていない API のイベントには、これらのフィールドがそもそも現れない。

👉 HTTP API の Lambda オーソライザーには 応答 10 秒の上限がある。オーソライザーの中で外部の認証基盤に問い合わせる設計だと、ここが詰まりどころになる。


{proxy+} と ANY

ルートを1つずつ定義する代わりに、まとめて受ける書き方がある。

  • {proxy+} — 階層パス全体にマッチする。/produce、/produce/fruit、/produce/vegetables/carrot のいずれにも当たる
  • {custom} — 1つのパスセグメントだけにマッチする
  • ANY — HTTP メソッドのプレースホルダ

組み合わせた ANY /{proxy+} は「全メソッド・全パスを1つの Lambda に流す」設定になる。Express や Laravel のようなルーティング機構を持つフレームワークを Lambda に載せる場合はこの形になる。

👉 REST API のリソース数上限は 300。{proxy+} はこの枠を節約する手段でもある。

👉 プロキシ統合ではリクエストパラメータの順序は保持されない。順序に依存する実装をしてはならない。


REST API と HTTP API のどちらを選ぶか

先に結論を書く。

HTTP API で足りるなら HTTP API。足りない機能が1つでもあるなら REST API。

名前が紛らわしいが、どちらも RESTful な HTTP API を作るための製品だ。違うのは機能の多さと値段でしかない。

❌ 「HTTP API のほうが名前が新しいから、これから作るなら常に HTTP API」は誤解。

HTTP API は REST API の上位互換ではない。 機能を削って安くした廉価版だ。だから「安いほう」を選んだあとに要件が足せない、ということが起きる。


HTTP API では使えないもの

判断に効くのはここだ。以下は REST API にはあって HTTP API には無い。

分類 HTTP API で使えない機能
セキュリティ AWS WAF、バックエンド認証用証明書
認可 リソースポリシー
配布・課金 API キー、使用量プラン、クライアント単位のレート制限、デベロッパーポータル
エンドポイント エッジ最適化エンドポイント、プライベートエンドポイント
開発 リクエスト検証、リクエストボディ変換、カスタムゲートウェイレスポンス、カナリアリリースデプロイ、テスト呼び出し、モック統合
性能 キャッシュ、レスポンスストリーミング
監視 X-Ray トレーシング、実行ログ、Amazon Data Firehose へのアクセスログ

逆に、HTTP API にしかないものもある。

  • JWT オーソライザー(REST API では Lambda オーソライザーを書いて自前で JWT を検証する)
  • 自動デプロイ(REST API は明示的なデプロイ操作が要る)
  • AWS Cloud Map へのプライベート統合

両方にあるもの: mTLS、IAM 認可、Cognito、Lambda オーソライザー、カスタムドメイン、CORS 設定、CloudWatch メトリクス、CloudWatch Logs へのアクセスログ、リクエストパラメータ変換、NLB / ALB へのプライベート統合。


値段の差

米国東部(バージニア北部)、100 万リクエストあたり。

REST API HTTP API
最初の 3 億件 / 月 $3.50(最初の 3 億 3,300 万) $1.00(最初の 3 億)
その次 $2.80(次の 6 億 6,700 万) $0.90
10 億超 / 月 $2.38 $0.90

約 3.5 倍の差がある。データ転送(アウト)は $0.09/GB で共通。REST API のキャッシュはキャッシュサイズに応じた別建ての時間課金になる。

👉 無料利用枠(12 か月)は REST API 100 万コール/月、HTTP API 100 万コール/月がそれぞれ用意されている。ただしこの 12 か月枠はアカウント作成時期に依存する。新しいアカウントはクレジット方式に切り替わっている場合があるので、現行の条件は必ず料金ページで確認すること。

👉 料金は 2026-08-11 時点、us-east-1 の値。リージョンで変わるし改定もある。実際の見積もりは必ず料金ページで確認すること。


判断の順序

上から順に見て、最初に当たったところで決まる。

  1. WAF / API キー / キャッシュ / リクエスト検証 / プライベートエンドポイント のどれかが要るか → 要るなら REST API
  2. 統合の処理が 30 秒を超えうるか → 超えうるなら REST API(緩和申請の余地がある。次章で詳述)
  3. それ以外 → HTTP API

👉 1 に「X-Ray でトレースしたい」を加えてもいい。分散トレーシングが要件なら REST API になる。


ハマりどころ:制限値のミスマッチ

この構成のつまずきは、ほとんどが同じ形をしている。

API Gateway 側の上限と Lambda 側の上限が違っていて、小さいほうが先に効く。

効く順に4つ見ていく。


(a) タイムアウト:手前が先に切る

上限 緩和
Lambda 関数タイムアウト 900 秒(15 分) —
REST API 統合タイムアウト(リージョン / プライベート) 50 ミリ秒 〜 29 秒 申請可(※)
REST API 統合タイムアウト(エッジ最適化) 50 ミリ秒 〜 29 秒 不可
HTTP API 統合タイムアウト 30 秒 不可

※ AWS の注記: 統合タイムアウトを 50 ミリ秒未満にはできない。29 秒より大きくすることはできるが、その場合アカウントのリージョンレベルのスロットル枠の削減が必要になることがある。

❌ 「Lambda のタイムアウトを 900 秒にすれば、長い処理も通る」は誤解。

手前の API Gateway が先に切る。 クライアントには 504 が返るが、Lambda はそのまま動き続けて課金される。 処理は完走するのにレスポンスだけ届かない、という一番デバッグしづらい状態になる。

❌ 「API Gateway は 29 秒で必ず切れる」も誤解。API 種別で違う。

  • HTTP API は 30 秒で、緩和申請そのものができない
  • REST API のリージョン / プライベートは緩和申請の余地がある。ただしスロットル枠とのトレードオフになる
  • REST API のエッジ最適化は 29 秒固定

👉 そもそも 30 秒に迫る処理を同期 API に載せている時点で設計を疑ったほうがいい。キューに投入して 202 を返し、結果は別途取りに来てもらう非同期化を先に検討する。


(b) ペイロードサイズ:10 MB は通らない

上限
API Gateway(REST / HTTP とも) 10 MB
Lambda 同期呼び出し リクエスト・レスポンス各 6 MB

👉 実効上限は小さいほうの 6 MB。 API Gateway の 10 MB を通っても Lambda の手前で弾かれる。

参考までに Lambda の他のペイロード上限も挙げておく。

  • 非同期呼び出し: 1 MB
  • レスポンスストリーミング(同期): 200 MB

👉 ファイルアップロード / ダウンロードを Lambda 経由でやろうとすると、この 6 MB に必ずぶつかる。S3 の署名付き URL をクライアントに渡して直接やり取りさせるのが定石だ。


(c) 同時実行:API Gateway と Lambda で桁が違う

デフォルト
API Gateway スロットル(アカウント / リージョン、HTTP・REST・WebSocket 合算) 10,000 RPS + トークンバケット方式のバースト(バケット最大容量 5,000 リクエスト)
Lambda 同時実行数(アカウント / リージョン) 1,000

👉 AWS 自身がドキュメントでこのミスマッチを明記している。 「API Gateway のデフォルトスロットル上限は 10,000 RPS、一方 Lambda のデフォルト同時実行上限は 1,000。このミスマッチにより、Lambda が処理できる以上のリクエストが API Gateway から来る可能性がある」

Lambda 側が溢れると 429 が返る。

👉 API Gateway 側のバースト枠は、AWS のサービスチームがアカウントの RPS 枠に応じて決める。顧客側で制御も変更申請もできない。

同時実行数と RPS は別物

❌ 「同時実行 1,000 なら 1,000 RPS まで捌ける」は誤解。

同時実行数はこう決まる。

同時実行数 = 平均 RPS × 平均処理時間(秒)
  • 処理時間 1 秒の関数 → 100 RPS で同時実行 100
  • 処理時間 500 ミリ秒の関数 → 100 RPS で同時実行 50
  • 処理時間 200 ミリ秒の関数 → 同時実行 1,000 で 5,000 RPS 捌ける

処理が速いほど、少ない同時実行数で多くの RPS を捌ける。

ただし別の上限がかかる。

RPS 上限 = 同時実行数 × 10

処理時間 50 ミリ秒の関数を考える。計算上は同時実行 1,000 で 20,000 RPS 捌けるはずだが、RPS 上限 10,000 で頭打ちになり、残りはスロットリングする。

👉 処理時間が 100 ミリ秒を切る関数では、同時実行数ではなく RPS 上限が先に効く。 この場合は同時実行数の上限緩和を申請するしかない(緩和すれば RPS 上限も 10 倍で連動する)。

スケールする速さにも上限がある

各リージョン・各関数につき、10 秒あたり 1,000 実行環境。

  • 未使用分は繰り越されない。10 秒間まったく呼ばれなくても、次の 10 秒の枠は 1,000 のままだ
  • 関数レベルの制限なので、関数ごとに独立してスケールする

👉 瞬間的に数千 RPS が立ち上がるようなスパイクでは、同時実行の上限に達する前にスケール速度のほうが先に効く。

同時実行の制御手段は2つ

予約された同時実行 プロビジョニングされた同時実行
何をするか その関数用に枠を確保する 実行環境を事前に初期化しておく
効き方 上限かつ下限として働く 事前初期化した数だけコールドスタートを回避
追加課金 なし あり
溢れたら スロットリング(429) 予約を併用していなければ未予約枠にはみ出す(コールドスタートは起きる)
  • 予約された同時実行はデフォルトで合計 900 まで。予約なしの関数用に常に 100 が確保されるため、全部は予約できない
  • プロビジョニングされた同時実行は設定後すぐには効かない。1〜2 分の準備を経て、1 分あたり最大 6,000 実行環境まで割り当てられる

👉 Function URL のスロットリング手段は予約された同時実行しかない。最大 RPS = 予約同時実行数 × 10 で、超えると 429。予約を 0 にすれば全リクエストが 429 になるので、緊急停止のスイッチとして使える。


(d) コールドスタート:思ったほど遅くないが、10 秒の壁がある

実行環境のライフサイクルは3フェーズ。

Init      拡張機能の起動 → ランタイムのブートストラップ → 関数の静的コード(ハンドラ外)の実行
Invoke    ハンドラの実行
Shutdown  終了処理

このうち Init が「コールドスタート」と呼ばれる部分だ。課金対象時間に含まれる。

❌ 「コールドスタートは常に数秒かかる」は誤解。AWS 公式の記述はこうだ。

コールドスタートは全呼び出しの 1% 未満で発生する。継続時間は 100 ミリ秒未満から 1 秒超まで幅がある。

👉 開発・テスト環境のほうが本番より起きやすい。呼び出し頻度が低いからだ。「開発中に体感した遅さ」を本番の見積もりに使うと過大評価になる。

Init は 10 秒で打ち切られる

Init フェーズには 10 秒の制限がある。 3つのタスクが 10 秒以内に終わらないと、Lambda は最初の呼び出し時に、関数のタイムアウト設定で Init をやり直す。

👉 ハンドラ外で重い初期化(大きな SDK の import、複数の接続確立)をやりすぎると、ここで詰まる。

👉 プロビジョニングされた同時実行 / SnapStart を使う場合はこの 10 秒制限が適用されず、130 秒または関数タイムアウト(最大 900 秒)の大きいほうまで許される。

実行環境が再利用される、ということ

Invoke が終わっても実行環境はすぐには消えない。凍結されて次の呼び出しを待つ。ここから3つの帰結が出る。

① ハンドラ外に置いたものは残る

// ここは Init で1回だけ実行される。コネクションが使い回される
const db = createConnection();

export const handler = async (event) => {
  return { statusCode: 200, body: JSON.stringify(await db.query(...)) };
};

② グローバル変数に呼び出し固有の状態を置くと漏れる

同じ実行環境が別のリクエストを処理するので、前のリクエストの値が残る。呼び出しごとにリセットされる前提で書いてはならない。

③ /tmp も残るが、永続ストレージではない

/tmp(512 MB 〜 10,240 MB)の内容は凍結中も残り、複数呼び出しにまたがる一時キャッシュとして使える。

👉 ただし Lambda は継続的に呼ばれている関数でも数時間ごとに実行環境を終了する。 ランタイムの更新とメンテナンスのためだ。実行環境が永続すると想定してはならない。

👉 呼び出し中にクラッシュ・タイムアウトすると Lambda は実行環境をリセットするが、リセットは /tmp の内容を消さない。 前の呼び出しの残骸が見えることがある。

👉 関数が終了した時点で完了していなかったバックグラウンド処理やコールバックは、実行環境が再利用されると再開する。 「投げっぱなしにしても Lambda が終われば止まる」わけではない。

緩和策

手段 追加課金 制約
プロビジョニングされた同時実行 あり —
SnapStart Java は追加課金なし。 Python 3.12 以降 / .NET 8 以降 はキャッシュ(関数バージョンごと、最低 3 時間分課金)と復元のたびに課金 Java 11 以降 / Python 3.12 以降 / .NET 8 以降 のみ対応。同一関数バージョンでプロビジョニングされた同時実行と併用不可
静的初期化の削減 なし 必要なクライアントだけ import する、遅延ロードする

ログはどこに出るのか

EC2 + nginx なら access.log と error.log の 2 つを見れば済む。この構成では 3 か所に分かれる。

ログ 出どころ デフォルト
Lambda の実行ログ Lambda が CloudWatch Logs に送る 有効(実行ロールに権限があれば)
API Gateway のアクセスログ 誰がどう叩いたか。自分で用意したロググループへ 無効
API Gateway の実行ログ API Gateway 内部の処理トレース。API Gateway が作るロググループへ 無効(REST API のみ)

❌ 「マネージドサービスだからログは勝手に全部出ている」は誤解。

デフォルトで出ているのは Lambda の分だけだ。API Gateway 側は 2 種類とも明示的に有効化しないと 1 行も残らない。


Lambda のログ

デフォルトで全呼び出しのログが CloudWatch Logs に送られる。ただし条件がある。

  • ロググループ名は /aws/lambda/<関数名>(別のロググループに送る設定もできる)
  • 実行ロールに logs:CreateLogGroup / logs:CreateLogStream / logs:PutLogEvents の 3 つが要る
  • この 3 つはマネージドポリシー AWSLambdaBasicExecutionRole に含まれている

👉 権限が無いと、関数は普通に動くのにログだけが出ない。 「関数が呼ばれていないのか、ログが出ていないだけなのか」で悩んだら、まず実行ロールを見る。

👉 料金は「Lambda のログ利用自体に追加料金はないが、CloudWatch Logs の標準料金がかかる」。ログは無料ではない。

👉 呼び出しからログが表示されるまで 5〜10 分かかることがある。 出ないと決めつける前に少し待つ。


API Gateway の 2 種類のログ

REST API には実行ログとアクセスログがある。この 2 つは互いに独立して有効化できる。

実行ログは API Gateway 内部の動きを追うためのものだ。

  • ロググループは API Gateway が作って管理する。 名前は API-Gateway-Execution-Logs_{rest-api-id}/{stage_name} 形式
  • 記録されるもの: エラー、実行トレース(リクエスト / レスポンスのパラメータ値やペイロード)、Lambda オーソライザーが使うデータ、API キーが必要か、使用量プランが有効か、など
  • レベルは Off / Errors only / Errors and info logs の 3 段階

👉 認可ヘッダー、API キーの値、および同様の機微なリクエストパラメータは、API Gateway が自動でログから編集(redact)する。

👉 ただし データトレース(Data tracing) は別だ。トラブルシューティングには有用だが、機微なデータをログに出力する結果になりうる。AWS は「本番環境の API では Data tracing を使わないことを推奨する」と明記している。

アクセスログは「誰がいつ何を叩いたか」を残すためのものだ。nginx の access.log に相当する。

  • ロググループは自分で用意する(既存のものを選んでもいい)
  • $context 変数を並べてログ形式を組み立てる。CLF / JSON / XML / CSV の例が用意されている
  • 形式には最低でも $context.requestId か $context.extendedRequestId を含めなければならない

requestId と extendedRequestId の違い

障害調査でどちらを追うかは、この違いで決まる。

$context.requestId $context.extendedRequestId
返るヘッダー x-amzn-RequestId x-amz-apigw-id
生成元 クライアントが指定できる API Gateway が生成する
クライアントによる上書き できる(UUID 形式なら) できない

❌ 「リクエスト ID があるからリクエストを一意に追える」は誤解。

requestId はクライアントが上書きできる。 UUID 形式でない値で上書きされた場合、アクセスログ上では {UUID}_REPLACED_INVALID_REQUEST_ID に置換される。

👉 extendedRequestId は呼び出し側が触れない。 AWS サポートに問い合わせるときに提供を求められるのもこちらだ。

👉 AWS のベストプラクティスは「両方を含めること」。片方だけにしない。


有効化に要る権限(ここでつまずく)

REST API のログにはアカウント単位の設定が要る。

  • apigateway.amazonaws.com を信頼エンティティとする IAM ロールを作り、マネージドポリシー AmazonAPIGatewayPushToCloudWatchLogs を付ける
  • そのロール ARN を Account の cloudWatchRoleArn に設定する

👉 cloudWatchRoleArn は、CloudWatch Logs を有効にしたいリージョンごとに個別に設定する必要がある。 東京で設定したから大阪でも効く、とはならない。

👉 API Gateway はこの IAM ロールを引き受けるために AWS STS を呼ぶ。そのリージョンで STS が有効になっている必要がある。

👉 ステージ設定・ログ・ステージ変数を更新した場合、API の再デプロイは不要。


HTTP API のログは選択肢が狭い

REST API HTTP API
CloudWatch Logs へのアクセスログ ○ ○
実行ログ ○ ×
Amazon Data Firehose へのアクセスログ ○ ×
AWS X-Ray トレーシング ○ ×
CloudWatch メトリクス ○ ○

HTTP API で設定できるのはアクセスログだけだ。

  • ロググループは自分で作り、その ARN をステージに指定する
  • 形式の例は REST API と同じく CLF / JSON / XML / CSV。ただし REST API の $context.resourcePath に当たる位置には $context.routeKey を使う
  • REST API と違い、アカウントレベルの cloudWatchRoleArn の設定は要求されない

👉「判断の順序」で「X-Ray が要るなら REST API」と書いたのはこれが理由だ。HTTP API を選ぶと、API Gateway 内部で何が起きたかを追う手段がアクセスログしか残らない。


Lambda をどの VPC に置くのか

❌ 「Lambda は VPC の外にあるサービスだ」は誤解。

すべての Lambda 関数は、Lambda サービスが所有・管理する VPC の中で動いている。 ただしこの VPC は Lambda が自動的に維持していて、顧客からは見えない。

👉 関数を自分の VPC のリソースにアクセスさせる設定をしても、関数が動いている Lambda 管理 VPC 自体には何の影響もない。 「VPC に入れる」という操作は、実際には「自分の VPC への通り道を作る」ことを意味する。


「VPC に入れる」と外に出られなくなる

デフォルトでは Lambda 関数はパブリックインターネットにアクセスできる。

VPC に接続すると、その VPC 内で利用可能なリソースにしかアクセスできなくなる。

つまり VPC 接続は、プライベートリソースへの経路を得る代わりに、インターネットへの経路を失うトレードオフだ。外部 API を叩いている関数を安易に VPC に入れると、その瞬間にタイムアウトし始める。

👉 症状は「エラー」ではなく「タイムアウト」として出る。接続先が見つからないのではなく、パケットが返ってこない。


❌ パブリックサブネットに置けばインターネットに出られる

出られない。 AWS のドキュメントは 2 か所で明記している。

パブリックサブネットに関数を接続しても、インターネットアクセスもパブリック IP アドレスも得られない。

EC2 の常識が通じないところだ。EC2 ならパブリックサブネットに置いてパブリック IP を付ければ外に出られるが、Lambda の ENI にはパブリック IP が付かない。

正解はこうなる。

              インターネット
                   ↑
                   │  IGW
┌──────────────────────────────────────┐
│ パブリックサブネット                 │
│ NAT ゲートウェイ                     │
│ ルート: 0.0.0.0/0 → IGW              │
└──────────────────────────────────────┘
                   ↑
                   │  0.0.0.0/0
┌──────────────────────────────────────┐
│ プライベートサブネット               │
│ Lambda(Hyperplane ENI)             │
│ ルート: 0.0.0.0/0 → NAT ゲートウェイ │
└──────────────────────────────────────┘
  • 関数はプライベートサブネットに接続する
  • プライベートサブネットのルートテーブルで 0.0.0.0/0 を NAT ゲートウェイに向ける
  • NAT ゲートウェイ自体はパブリックサブネットに置く(ルートテーブルのエントリはプライベートサブネット側に書く、というねじれに注意)
  • パブリックサブネットのルートテーブルは 0.0.0.0/0 をインターネットゲートウェイに向ける
  • セキュリティグループはアウトバウンドを許可するものを選ぶ

👉 この 2 つのルートは別々のルートテーブルで満たす必要がある。1 つのルートテーブルに両方は書けない。

👉 IPv6 を使う場合、プライベートサブネット側は ::/0 をエグレス専用インターネットゲートウェイに向ける。

👉 可用性のため AZ は最低 2 つ。NAT ゲートウェイは「1 AZ につき 1 つ」が回復性の面で推奨されている。

👉 S3 の Gateway 型 VPC エンドポイントには費用がかからない。 S3 にアクセスするためだけに NAT ゲートウェイを通す必要はない。


Hyperplane ENI というもの

Lambda が VPC 接続のために作るネットワークインターフェイスは Hyperplane ENI と呼ばれる。Lambda が自動で作成・管理し、直接は見えず、設定も管理も不要だ。

見えないが、挙動は知っておく必要がある。

① サブネットとセキュリティグループの組み合わせごとに 1 つ作られ、共有される

同じアカウント内の別の関数が同じ組み合わせを使えば、その ENI を共有できる。Lambda は可能な限り既存の ENI を再利用する。

👉 性能上のベストプラクティスはここから来る。既に VPC に接続した関数があるなら、別の関数も同じサブネットとセキュリティグループを指定する。 新しい ENI の作成を避けられる。

② 1 つの ENI で最大 65,000 接続 / ポート

超えると、Lambda がトラフィックと同時実行の要求に応じて ENI 数を自動的にスケールする。

③ 作成中は関数を呼び出せない

新規関数では、Hyperplane ENI の作成中は関数が Pending 状態になり呼び出せない。Active になるまで数分かかることがある。

既存関数の場合、その間はバージョン作成やコード更新ができない。ただし以前のバージョンの呼び出しは継続できる。

④ 14 日アイドルで回収される

関数が 14 日間アイドル状態だと、Lambda は未使用の Hyperplane ENI を回収し、関数の状態を Inactive にする。次の呼び出しは失敗し、関数は ENI の作成・割り当てが終わるまで Pending に戻る。

👉 AWS は「ENI が永続することに依存した設計にしないこと」を推奨している。負荷分散やヘルスチェックの都合で、Lambda が ENI を削除して作り直すこともある。

👉 めったに呼ばれない VPC 接続関数(月次バッチなど)は、この 14 日にぶつかりうる。

⑤ 外すのに最大 20 分かかる

VPC 設定を外すと、Hyperplane ENI の削除に最大 20 分かかる。他の関数(や公開済みの関数バージョン)がその ENI を使っていない場合にのみ削除される。

👉 Lambda は関数の実行ロールの権限を使って ENI を削除する。ENI の削除前に実行ロールを消すと、Lambda は ENI を削除できなくなる。 関数を片付けるときは、実行ロールを最後に消す。


権限には落とし穴がある

実行ロールに次の 6 つが要る。

ec2:CreateNetworkInterface
ec2:DescribeNetworkInterfaces
ec2:DescribeSubnets
ec2:DeleteNetworkInterface
ec2:AssignPrivateIpAddresses
ec2:UnassignPrivateIpAddresses

マネージドポリシー AWSLambdaVPCAccessExecutionRole で付与できる。

👉 この権限は ENI の作成のためだけに必要で、関数の呼び出しには不要。 実行ロールから外しても、VPC に接続済みの関数の呼び出しは成功する。

❌ 「実行ロールの権限は Lambda サービスが使うだけ」は誤解。

これらの権限は関数のコードにも暗黙的に付与されている。つまり関数のコードがこれらの Amazon EC2 API 呼び出しを実行できる。

👉 最小権限にするには、lambda:SourceFunctionArn 条件キーを使った Deny ポリシーを実行ロールに足す。この条件キーは関数コードの実行中に行われた API 呼び出しにだけ適用されるので、Lambda サービス側の VPC リソース管理は妨げない。


そもそも VPC に入れるべきか

判断はシンプルだ。

接続先 VPC に入れるか
RDS / ElastiCache など VPC 内のリソース 入れる(それ以外に届く手段がない)
S3 / DynamoDB / 外部 API だけ 入れない(入れると NAT ゲートウェイが要る)

👉 VPC に入れると付いてくるもの: NAT ゲートウェイの費用、ENI のライフサイクル(Pending / Inactive / 削除 20 分)、そして VPC あたり ENI 500 という上限(Amazon EFS など他サービスと共有。数千まで緩和可)。

👉 その他の制約として、専有インスタンステナンシーの VPC には直接接続できない。デフォルトテナンシーの別 VPC とピアリングする必要がある。

👉 組織として強制したい場合は IAM 条件キー lambda:VpcIds / lambda:SubnetIds / lambda:SecurityGroupIds が使える。「全関数を VPC 接続必須にする」「特定のサブネットしか使わせない」といった制御ができる。


EC2 / ALB 構成と比べていつ選ぶか

ALB + Lambda という第三の道

既に ALB がある環境なら、一部のパスだけを Lambda に流す手がある。ただし制限は API Gateway 経由より厳しい。

  • リクエストボディの最大 1 MB、レスポンス JSON の最大 1 MB(API Gateway 経由の 6 MB より小さい)
  • WebSocket 非対応。 Upgrade リクエストは HTTP 400 で拒否される
  • Lambda 関数とターゲットグループは同一アカウント・同一リージョンである必要がある
  • ターゲットグループ 1 つにつき Lambda 関数は 1 つ。エイリアスを含む ARN で登録するのが推奨
  • イベント形式は API Gateway と違う(requestContext.elb.targetGroupArn を持つ独自形式)。レスポンスには statusDescription フィールドがある
  • マルチバリューヘッダーはターゲットグループ属性 lambda.multi_value_headers.enabled で明示的に有効化する。無効のままだと最後の値だけが使われる
  • ヘルスチェックはデフォルト無効。有効にすると ELB-HealthChecker/2.0 の User-Agent でイベントが来るが、このヘルスチェックも通常の呼び出しと同様に課金される

👉 ALB のセキュリティグループにアウトバウンドルールが要らない理由は前述のとおり(ALB は接続を張らずに直接 Lambda を呼び出すため)。

👉 ALB のアクセスログについては別記事にまとめた。

常駐型(EC2 / ECS)と比べる軸

金額の試算は載せない。単価も構成も変わるからだ。代わりに判断の軸を挙げる。

① トラフィックの形

  • 常時ほぼ一定の負荷 → 常駐型が有利になりやすい
  • バースト・間欠・夜間ゼロ → Lambda が有利になりやすい

② 課金構造の違い

  • API Gateway + Lambda はリクエスト数 × 単価(+ Lambda の実行時間)
  • 常駐型はインスタンス時間 × 単価(リクエストが 0 でも発生する)

👉 損益分岐はこの2つの掛け算の大小で決まる。「サーバーレスは安い」も「サーバーレスは高くつく」も、トラフィック量を入れずには言えない。

③ 同期処理の長さ

30 秒に収まらない同期処理があるなら、そもそも構成を見直す。API Gateway + Lambda は「短い同期処理」に最適化されている。

④ 状態を持てるか

実行環境は数時間で破棄され、リクエストがどの実行環境に当たるかは選べない。セッションをメモリに持つ設計は成立しない。 外部(DynamoDB / ElastiCache など)に出す必要がある。

⑤ 運用の中身が入れ替わる

OS のパッチ当てとミドルウェアの更新は消える。代わりに、この記事で挙げた制限値との付き合いが増える。ゼロになるのではなく入れ替わる。


まとめ

Lambda には HTTP のリスナーがない。 あるのは Invoke API だけだ。だから HTTP を Invoke に翻訳する誰かが必ず要る。API Gateway はその翻訳機であり、ついでに認証・スロットリング・検証を引き受けている。

そしてこの構成のハマりどころは、ほぼ全部**「翻訳機と実行環境の制限値がズレていて、小さいほうが先に効く」**ことに帰着する。

何を選ぶか

要件 選ぶもの
HTTP で叩ければいい。認証は IAM か無しでいい Function URL
認証方式の選択肢・スロットリング・カスタムドメインが要る API Gateway
↳ WAF / API キー / キャッシュ / リクエスト検証 / X-Ray / プライベートが要る REST API
↳ 上記が不要 HTTP API(REST API の約 1/3.5 の値段)
既に ALB があり、req / res とも 1 MB に収まる ALB のターゲット

構築前に決めておくこと

① 設計前に確認する4つの数字

数字 意味
6 MB リクエスト / レスポンスの実効上限(API Gateway は 10 MB だが Lambda が 6 MB)
30 秒 HTTP API の統合タイムアウト。緩和不可。REST API は 29 秒だが申請の余地あり
1,000 並列 Lambda のデフォルト同時実行数。溢れると 429。RPS 上限はこの 10 倍
10 秒あたり 1,000 環境 スケールできる速さ。スパイクではここが先に効く

② ログを 3 か所とも有効にしたか

デフォルトで出るのは Lambda の分だけだ。API Gateway のアクセスログと実行ログは、有効化しない限り 1 行も残らない。障害が起きてから有効化しても、そのときのログはもう無い。

  • アクセスログの形式に $context.requestId と $context.extendedRequestId を両方入れたか
  • REST API なら、使うリージョンごとに cloudWatchRoleArn を設定したか
  • Lambda の実行ロールに CloudWatch Logs への書き込み権限があるか

③ VPC に入れる必要が本当にあるか

  • 接続先が RDS / ElastiCache など VPC 内のリソースなら入れる。それ以外に届く手段がない
  • S3 / DynamoDB / 外部 API だけなら入れない。入れると NAT ゲートウェイが要る
  • 入れるならプライベートサブネットに置く。パブリックサブネットに置いてもインターネットには出られない

これらを確認してから、構築に入る。


参考

すべて 2026-08-11 時点。

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?