【AWS】Lambda + API Gatewayでサーバーレスなエンドポイントを作る|Node.js・デプロイ・コールドスタート対策
はじめに
「小さな API をサーバーなしで公開したい」「EC2 やコンテナの運用コストを抑えたい」といった場面で、AWS Lambda と API Gateway の組み合わせはよく検討されます。HTTP リクエストを受け取り、Lambda 関数を呼び出し、レスポンスを返すまでをマネージドサービスに任せられるため、トラフィックが少ない API やプロトタイプに向いています。
一方で、初めて触れると次の点でつまずきやすいです。
- API Gateway の種類(HTTP API と REST API)の違い
- Lambda ハンドラが返すべきレスポンス形式
- デプロイ手順と実行ロール(IAM)の設定
- コールドスタートによる初回レスポンスの遅延
この記事では、Node.js で Lambda 関数を書き、API Gateway(HTTP API)と接続して公開する流れを整理します。デプロイは AWS SAM を例にし、コールドスタートを抑える実務的なポイントと、よくある失敗例もあわせて扱います。
背景:Lambda と API Gateway の役割
処理の流れ
クライアントが API Gateway の URL に HTTP リクエストを送ると、API Gateway がルートに応じて Lambda 関数を呼び出します。Lambda はイベントオブジェクトを受け取り、処理結果を返します。API Gateway がその結果を HTTP レスポンスとしてクライアントへ返します。
クライアント
→ API Gateway(ルーティング・認証など)
→ Lambda(ビジネスロジック)
← API Gateway
← クライアント
本記事では、複数ルートや CORS をまとめて管理しやすい API Gateway(HTTP API) を前提にします。Lambda 単体に HTTP エンドポイントを付ける 関数 URL もありますが、ルート単位の制御が少ない用途向けです。比較は HTTP リクエストで Lambda を呼び出す方法の選択を参照してください。
HTTP API と REST API の選び方
API Gateway には主に HTTP API と REST API があります。統合とは、API Gateway が Lambda に渡すリクエスト情報の形式と、Lambda から受け取るレスポンスの解釈ルールのことです。本記事で使う プロキシ統合は、HTTP リクエストの内容をほぼそのまま Lambda の event に載せ、Lambda の返り値を HTTP レスポンスに変換する方式です。
REST API と HTTP API の比較によると、REST API は API キー、リクエスト検証、AWS WAF 連携など機能が豊富です。HTTP API は機能を絞った構成で、料金面で有利な場合が多いとされています。
| 観点 | HTTP API | REST API |
|---|---|---|
| 主な用途 | Lambda へのシンプルなプロキシ | 細かい認可・検証・変換が必要な API |
| Lambda 連携 | プロキシ統合が標準 | AWS_PROXY 統合など |
| API キー・使用量プラン | 非対応 | 対応 |
| 料金 | 比較的低め | 機能に応じて高めになりやすい |
Lambda にリクエストをそのまま渡して JSON を返すだけの API であれば、HTTP API から始める選択が一般的です。API キーやリクエストボディの厳密な検証が必須なら REST API を検討します。本記事のコードは HTTP API のプロキシ統合のみを対象とします。
実装と検証
最小の Lambda ハンドラ(Node.js)
コード例のランタイムは Node.js 22(nodejs22.x)を想定しています。ランタイムのサポート状況は Lambda ランタイム一覧で確認してください。
HTTP API の Lambda プロキシ統合では、API Gateway がイベントを JSON で渡します。ペイロード形式は 1.0 と 2.0 があり、HTTP API の Lambda 統合で構造の差が説明されています。新規の HTTP API では 2.0 がよく使われます。
GET /hello?name=world に相当するリクエストでは、event の主要フィールドは次のとおりです(抜粋)。
{
"version": "2.0",
"routeKey": "GET /hello",
"rawPath": "/hello",
"queryStringParameters": { "name": "world" },
"requestContext": { "requestId": "..." }
}
本記事では、形式の違いに左右されないよう、レスポンスは常に statusCode、headers、body を明示する形に統一します。
// src/handler.mjs
// 想定リクエスト: GET /hello?name=world
export const handler = async (event) => {
// queryStringParameters: URL のクエリ文字列(?name=world)
const name = event.queryStringParameters?.name ?? "guest";
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({
message: `Hello, ${name}`,
// requestContext: API Gateway が付与するメタ情報
requestId: event.requestContext?.requestId,
}),
};
};
実装時の注意点:
- Node.js 24 以降のランタイムでは、コールバック形式のハンドラはサポートされない。
async関数でreturnする書き方に統一する(Node.js 24 ランタイムの説明)。 bodyは文字列である。オブジェクトをそのまま返すと API Gateway 側でエラーになる。- ブラウザから呼ぶ場合は、HTTP API 側で CORS を設定する方法が一般的である。SAM では
GlobalsのApiセクションやHttpApiリソースで設定できる。今回の/hello単体を curl で試すだけなら CORS は不要である。
AWS SDK クライアントはモジュール外で初期化する
外部サービスを呼ぶときの定番パターンとして、DynamoDB を例に示します。前節の /hello とは別ルート(GET /items/{id})を想定しています。
ウォームスタートとは、直前に使われた Lambda の実行環境を再利用する状態のことです。コールドスタート(新規コンテナの起動)は後述の章で扱います。
S3 や DynamoDB など AWS サービスを呼ぶ場合、クライアントをハンドラ内で毎回 new すると、ウォームスタート時でも無駄な初期化が増える。モジュールスコープで 1 回だけ作るパターンがよく紹介される。
// src/get-item.mjs
// 想定リクエスト: GET /items/abc123
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, GetCommand } from "@aws-sdk/lib-dynamodb";
const client = DynamoDBDocumentClient.from(new DynamoDBClient({}));
export const handler = async (event) => {
// pathParameters: ルート定義の {id} 部分
const id = event.pathParameters?.id;
if (!id) {
return {
statusCode: 400,
headers: { "content-type": "application/json" },
body: JSON.stringify({ error: "id is required" }),
};
}
const result = await client.send(
new GetCommand({
TableName: process.env.TABLE_NAME,
Key: { id },
})
);
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify(result.Item ?? {}),
};
};
Node.js 18 以降の Lambda ランタイムには AWS SDK for JavaScript v3 が含まれます。追加の npm パッケージとして同梱する場合は、デプロイパッケージのサイズに注意してください。
デプロイ(AWS SAM の例)
手動でコンソールから関数と API を作ることもできますが、再現性のために AWS SAM(Serverless Application Model)でまとめてデプロイする方法を示します。AWS SAM ドキュメントでも同様の構成が案内されています。
事前準備
次が整っていることを確認する。
aws --version
sam --version
aws sts get-caller-identity # 認証済みか確認
aws configure でアクセスキーまたは SSO の設定が済んでいることを前提とします。CloudFormation・Lambda・API Gateway・IAM ロール作成権限も必要です。SAM の初回デプロイでは、S3 バケットへアーティファクトを置く権限も求められる場合があります。
プロジェクト構成
本記事の /hello 例では npm 依存がないため、package.json は空でもよい。SAM の sam build が通るよう、最低限のファイルだけ置く。
hello-api/
├── src/
│ └── handler.mjs
└── template.yaml
template.yaml
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31 # SAM 拡張を有効化
Description: Lambda + HTTP API sample
Globals:
Function:
Runtime: nodejs22.x
Architectures:
- arm64 # 初回は x86_64 でも可。コスト試行の一例
Timeout: 10
MemorySize: 256
Resources:
HelloFunction:
Type: AWS::Serverless::Function
Properties:
# src/handler.mjs の export const handler に対応
Handler: src/handler.handler
CodeUri: .
Events:
GetHello:
Type: HttpApi # HTTP API とルートを自動作成
Properties:
Path: /hello
Method: GET
Outputs:
ApiEndpoint:
Description: HTTP API endpoint URL
Value: !Sub "<https://$>{ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/hello"
Events: HttpApi を付けると、SAM が HTTP API・ルート・Lambda 呼び出し権限をまとめて作成します。ServerlessHttpApi は SAM が暗黙に作る HTTP API の論理 ID です。
実行ロールと IAM
Lambda は 実行ロール(IAM ロール)を通じて他の AWS リソースへアクセスする。API Gateway から Lambda を呼ぶ権限は、SAM が Events: HttpApi から自動で付与する。
DynamoDB などを呼ぶ関数では、テンプレートに Policies を足して最小権限を与える。
HelloFunction:
Type: AWS::Serverless::Function
Properties:
Handler: src/handler.handler
Policies:
- DynamoDBReadPolicy:
TableName: !Ref ItemsTable
DynamoDBReadPolicy は SAM の組み込みポリシーです。独自の権限が必要なときは SAM ポリシーテンプレートを参照してください。
デプロイ手順
cd hello-api
sam build
sam deploy --guided
初回の sam deploy --guided では、スタック名、リージョン、変更セットの確認方法などを対話形式で設定する。2 回目以降は sam deploy だけで更新できる。
デプロイ後、Outputs の URL へ ?name=world を付けてアクセスする。
curl "<https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/hello?name=world>"
期待されるレスポンスの例は次のとおりです。
{"message":"Hello, world","requestId":"..."}
HTTP ステータスは 200 です。403 や 502 が返る場合は、デプロイリージョン・URL の誤り、スタックのロールバック、ハンドラの例外などを CloudWatch Logs で確認してください。
デプロイ時の注意点
- Lambda の実行ロールに、呼び出す AWS サービス(DynamoDB、S3 など)への最小権限を付与する。SAM では
Policiesプロパティでテンプレートに含められる。 CodeUriにnode_modulesごと含めるとパッケージが肥大化し、コールドスタートやデプロイ時間に影響しやすい。必要な依存だけをバンドルする方法を後述する。- 本番と検証でスタック(ステージ)を分ける運用が一般的である。SAM の
--config-envやパラメータで環境ごとの値を切り替える。
コールドスタート対策
コールドスタートは、しばらく呼び出されなかった Lambda コンテナを新たに起動するときに発生する初期化時間です。体感レイテンシは、ランタイム、メモリ割り当て、デプロイパッケージのサイズ、VPC 接続の有無などで変わります。特定の秒数を保証するものではないため、本番前に CloudWatch のメトリクスや実リクエストで計測するのが確実です。
初回デプロイ後は、次の順で試すとよい。
- デプロイパッケージのサイズ削減(esbuild バンドル)
- メモリとタイムアウトの見直し
- 初期化処理をハンドラ外へ寄せる
本番でレイテンシのばらつきが問題になったら、4 以降を検討する。
1. デプロイパッケージを小さくする
node_modules をそのまま zip するのではなく、esbuild などで 1 ファイルにバンドルすれば、読み込み対象を減らせ、起動も軽くなることがあります。SAM では既存の HelloFunction に Metadata を追加します。
HelloFunction:
Type: AWS::Serverless::Function
Metadata:
BuildMethod: esbuild
BuildProperties:
Minify: true
Target: es2022
EntryPoints:
- src/handler.mjs
Properties:
Handler: src/handler.handler
# ... 以下は従来どおり
2. メモリとタイムアウトを見直す
Lambda はメモリ割り当てに応じて CPU もスケールする。Lambda の設定を参照し、処理時間とコストのバランスを取る。タイムアウトは、API Gateway の統合タイムアウト(HTTP API では最大 30 秒)以内に収める。
3. 初期化処理をハンドラ外へ寄せる
DB コネクションや設定の読み込みは、リクエストごとではなくモジュール読み込み時に 1 回だけ行うパターンは有効な場合があります。接続を張りっぱなしにすると、同時実行数の増加に伴いリソースを消費するため、接続プールの上限は設計で決めます。
4. Provisioned Concurrency の検討(本番向け)
レイテンシのばらつきを抑えたい API では、Provisioned Concurrency で常時ウォームな実行環境を確保できる。常時課金が発生するため、トラフィックが安定している本番 API など、要件とコストを見合わせて使う。
5. VPC 接続は必要なときだけ(補足)
VPC 内の RDS などにアクセスする必要がない限り、Lambda を VPC に入れない構成のほうが、ネットワーク初期化によるコールドスタートの増加を避けやすい。VPC 接続が必要な場合は、Lambda の VPC ネットワークの前提(サブネット、セキュリティグループ、NAT など)を確認する。
よくある失敗例:レスポンス形式の不一致
本記事では statusCode + headers + body の明示形式を推奨している。次のようにオブジェクトだけを return するコードは、避ける。
// 誤り:本記事の推奨形式に合わない
export const handler = async (event) => {
return { message: "ok" };
};
HTTP API のペイロード形式 2.0 では、有効な JSON だけを返すと 200 として扱われる場合もある。ただし REST API の AWS_PROXY 統合や形式 1.0、テストコードでは期待どおりに動かないことが多い。
対処:
statusCode、headers、body(JSON 文字列)を明示する形に統一する。エラー時も同じ形で statusCode: 4xx を返し、アプリ側と API Gateway 側の契約を一貫させる。
まとめ
- Lambda と API Gateway を組み合わせると、サーバーレスな HTTP エンドポイントを短いコードで公開できる。
- シンプルな API なら HTTP API から始め、必要な機能が増えたら REST API を検討する。
- Node.js では
asyncハンドラと、API Gateway 向けのレスポンス形式(statusCode+body)を揃える。 - AWS SAM で関数と HTTP API をまとめてデプロイすると、権限とルート設定の再現がしやすい。
- コールドスタートはパッケージサイズ削減、メモリ調整、初期化の見直し、必要に応じた Provisioned Concurrency で扱う。
次に試すこと
- 上記の SAM テンプレートを手元の AWS アカウントにデプロイし、
/helloが応答することを確認する。 - CloudWatch の
DurationとInit Durationメトリクスを見て、コールドスタートとウォームスタートの差を把握する。 - 認証が必要なら、HTTP API の JWT オーソライザー(JWT オーソライザー)をテンプレートに追加する。





