【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 APIREST API があります。統合とは、API Gateway が Lambda に渡すリクエスト情報の形式と、Lambda から受け取るレスポンスの解釈ルールのことです。本記事で使う プロキシ統合は、HTTP リクエストの内容をほぼそのまま Lambda の event に載せ、Lambda の返り値を HTTP レスポンスに変換する方式です。

REST API と HTTP API の比較によると、REST API は API キー、リクエスト検証、AWS WAF 連携など機能が豊富です。HTTP API は機能を絞った構成で、料金面で有利な場合が多いとされています。

観点HTTP APIREST 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.02.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": "..." }
}

本記事では、形式の違いに左右されないよう、レスポンスは常に statusCodeheadersbody を明示する形に統一します。

// 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 では GlobalsApi セクションや 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 です。403502 が返る場合は、デプロイリージョン・URL の誤り、スタックのロールバック、ハンドラの例外などを CloudWatch Logs で確認してください。

デプロイ時の注意点

  • Lambda の実行ロールに、呼び出す AWS サービス(DynamoDB、S3 など)への最小権限を付与する。SAM では Policies プロパティでテンプレートに含められる。
  • CodeUrinode_modules ごと含めるとパッケージが肥大化し、コールドスタートやデプロイ時間に影響しやすい。必要な依存だけをバンドルする方法を後述する。
  • 本番と検証でスタック(ステージ)を分ける運用が一般的である。SAM の --config-env やパラメータで環境ごとの値を切り替える。

コールドスタート対策

コールドスタートは、しばらく呼び出されなかった Lambda コンテナを新たに起動するときに発生する初期化時間です。体感レイテンシは、ランタイム、メモリ割り当て、デプロイパッケージのサイズ、VPC 接続の有無などで変わります。特定の秒数を保証するものではないため、本番前に CloudWatch のメトリクスや実リクエストで計測するのが確実です。

初回デプロイ後は、次の順で試すとよい。

  1. デプロイパッケージのサイズ削減(esbuild バンドル)
  2. メモリとタイムアウトの見直し
  3. 初期化処理をハンドラ外へ寄せる

本番でレイテンシのばらつきが問題になったら、4 以降を検討する。

1. デプロイパッケージを小さくする

node_modules をそのまま zip するのではなく、esbuild などで 1 ファイルにバンドルすれば、読み込み対象を減らせ、起動も軽くなることがあります。SAM では既存の HelloFunctionMetadata を追加します。

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、テストコードでは期待どおりに動かないことが多い。

対処: statusCodeheadersbody(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 で扱う。

次に試すこと

  1. 上記の SAM テンプレートを手元の AWS アカウントにデプロイし、/hello が応答することを確認する。
  2. CloudWatch の DurationInit Duration メトリクスを見て、コールドスタートとウォームスタートの差を把握する。
  3. 認証が必要なら、HTTP API の JWT オーソライザー(JWT オーソライザー)をテンプレートに追加する。

Source