API Gateway HTTP API と Lambda Proxy 統合

動的バックエンドの基本パターンが API Gateway + Lambda。本記事では HTTP API と REST API の選び分け、Lambda Proxy 統合の仕組み、Lambda の設定(Node.js 20 / arm64 / 256MB / 10s)について書きます。

HTTP API vs REST API

API Gateway には 2 種類あります:

項目HTTP APIREST API
料金$1.00 / 100 万 req(最初)$3.50 / 100 万 req(最初)
レイテンシ低い
機能必要十分API Key / Usage Plan / Request Validator 等が豊富
用途シンプルな APIAPI as a Product(API 自体を商品化)
払い出される URLhttps://xxx.execute-api.region.amazonaws.com同上 + ステージ

本シリーズは「問い合わせフォーム 1 個」だけなので HTTP API 一択。料金 1/3 でレイテンシも低い。

Lambda Proxy 統合 (AWS_PROXY)

API Gateway → Lambda の繋ぎ方は複数ありますが、現代的なベストプラクティスは Lambda Proxy 統合:

これにより API Gateway 側の設定がシンプルになり、Lambda 内ですべて完結する設計が可能。

本シリーズの backend_api モジュール

このモジュールが作るリソース 11 個:

  1. aws_dynamodb_table
  2. data "archive_file" — Lambda zip パッケージング
  3. aws_cloudwatch_log_group — Lambda ログ
  4. aws_iam_role — Lambda 実行ロール
  5. aws_iam_role_policy — Lambda 用最小権限
  6. aws_lambda_function
  7. aws_apigatewayv2_api — HTTP API
  8. aws_apigatewayv2_stage — $default + throttling
  9. aws_apigatewayv2_integration — API GW → Lambda
  10. aws_apigatewayv2_route × 2 — POST と OPTIONS
  11. aws_lambda_permission — API GW から Lambda 呼出許可

API Gateway HTTP API 本体

resource "aws_apigatewayv2_api" "this" {
  name          = "${var.name_prefix}-api"
  protocol_type = "HTTP"             # HTTP API を意味する
  description   = "iigtn ${var.name_prefix} HTTP API"

  cors_configuration {
    allow_origins = [var.allowed_origin]   # https://lab.iigtn.com
    allow_methods = ["POST", "OPTIONS"]
    allow_headers = ["Content-Type"]
    max_age       = 300
  }
}

protocol_type = "HTTP" で HTTP API。"WEBSOCKET" もある。CORS は API Gateway 側でも設定可能(Lambda 側でも可)。

Stage($default で AutoDeploy)

resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.this.id
  name        = "$default"
  auto_deploy = true

  default_route_settings {
    detailed_metrics_enabled = true
    throttling_burst_limit   = 100
    throttling_rate_limit    = 50
  }
}

HTTP API では $default ステージを使うと URL が https://xxx.execute-api.../ のシンプルな形になります。ステージ名のサフィックス付きにならない。

Throttling 100 burst / 50 rps は個人サイトでは余裕。攻撃想定するならもっと厳しく。

Integration(API GW → Lambda)

resource "aws_apigatewayv2_integration" "contact" {
  api_id                 = aws_apigatewayv2_api.this.id
  integration_type       = "AWS_PROXY"               # Lambda Proxy 統合
  integration_uri        = aws_lambda_function.contact.invoke_arn
  integration_method     = "POST"
  payload_format_version = "2.0"                     # HTTP API 用 (REST は "1.0")
}

payload_format_version 2.0 はリクエスト構造が event.requestContext.http 配下に整理されています。新しい Lambda コードは 2.0 前提で書く。

Route(POST と OPTIONS)

resource "aws_apigatewayv2_route" "contact_post" {
  api_id    = aws_apigatewayv2_api.this.id
  route_key = "POST /api/contact"
  target    = "integrations/${aws_apigatewayv2_integration.contact.id}"
}

resource "aws_apigatewayv2_route" "contact_options" {
  api_id    = aws_apigatewayv2_api.this.id
  route_key = "OPTIONS /api/contact"
  target    = "integrations/${aws_apigatewayv2_integration.contact.id}"
}

OPTIONS は CORS preflight 用。API Gateway の cors_configuration が自動応答してくれますが、Lambda 側でも返せるように両方設定。

Lambda Permission(API GW から呼出許可)

resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.contact.function_name
  principal     = "apigateway.amazonaws.com"
  source_arn    = "${aws_apigatewayv2_api.this.execution_arn}/*/*"
}

これが無いと API GW から Lambda を呼び出そうとして AccessDenied になります。Lambda は IAM Role だけでは「他から呼べる」ようにならない。リソースベースポリシーで「特定の API GW からの InvokeFunction」を明示的に許可する必要がある。

Lambda 関数本体

resource "aws_lambda_function" "contact" {
  function_name    = "${var.name_prefix}-contact"
  description      = "Contact form handler"
  role             = aws_iam_role.lambda.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"          # Node.js 20 LTS
  architectures    = ["arm64"]              # Graviton (約 20% 安)
  timeout          = 10                     # 10 秒
  memory_size      = 256                    # 256 MB
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256

  environment {
    variables = {
      DDB_TABLE      = aws_dynamodb_table.contacts.name
      SES_FROM       = var.ses_from
      SES_TO         = var.ses_to
      ALLOWED_ORIGIN = var.allowed_origin
    }
  }

  depends_on = [aws_cloudwatch_log_group.contact]
}

Lambda 設定の選び方

項目選択理由
runtimenodejs20.xLTS。AWS SDK v3 がプリインストール
architecturesarm64 (Graviton)x86_64 より 約 20% 安く性能ほぼ同等
timeout10 秒SES 含めても十分。長くする意味なし
memory_size256 MBDDB+SES なら速い。CPU はメモリに比例なので増やすと高速になるが料金倍

archive_file で Lambda パッケージ作成

data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = var.lambda_source_dir       # ../../../backend/functions/contact
  output_path = "${path.module}/.build/contact_lambda.zip"
}

apply 時に自動で zip 化されます。output_base64sha256 をハッシュとして aws_lambda_function に渡すと、コード変更が検知されて自動更新される。

CloudWatch Log Group の事前作成

resource "aws_cloudwatch_log_group" "contact" {
  name              = "/aws/lambda/${var.name_prefix}-contact"
  retention_in_days = var.log_retention_days   # 14 日
  tags              = var.tags
}
Lambda の log group は 明示的に作らないと自動作成され、retention は無期限になる。これでログがたまり続けて月数百円〜千円課金されている人が多い。先に retention 付きで作っておくのが鉄則。

Lambda IAM Role の最小権限

data "aws_iam_policy_document" "lambda_policy" {
  # CloudWatch Logs (この log group のみ)
  statement {
    actions   = ["logs:CreateLogStream", "logs:PutLogEvents"]
    resources = ["${aws_cloudwatch_log_group.contact.arn}:*"]
  }

  # DynamoDB (このテーブルの PutItem のみ)
  statement {
    actions   = ["dynamodb:PutItem"]
    resources = [aws_dynamodb_table.contacts.arn]
  }

  # SES SendEmail
  statement {
    actions = ["ses:SendEmail", "ses:SendRawEmail"]
    resources = ["*"]
  }
}

「ログ書込 + DDB Put + SES Send だけ」の最小権限。他のリソースには触れない。

動作確認

curl -X POST "https://xxx.execute-api.ap-northeast-1.amazonaws.com/api/contact" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test","email":"test@example.com","message":"Hello world testing"}'

# {"ok":true}

次の記事

API Gateway + Lambda が動いたら、次は DynamoDB のテーブル設計 について書きます。問い合わせデータをどう保存するか、PII(個人情報)の扱い、PK 選定の考え方、PITR の有効化等を解説します。

📚 用語集

API Gateway
AWS のマネージド API エンドポイント。HTTP / REST / WebSocket を受け付けて Lambda 等のバックエンドに振り分ける。
HTTP API
API Gateway の新世代タイプ。REST API より約 1/3 の価格、低レイテンシ、シンプル。
REST API
API Gateway の旧世代タイプ。API Key / Usage Plan / Request Validator 等の高機能を持つが、料金が高め。
WebSocket API
API Gateway の WebSocket 対応タイプ。リアルタイム双方向通信用。
Lambda Proxy 統合 (AWS_PROXY)
API Gateway が HTTP リクエストを Lambda にそのまま渡し、Lambda が返す JSON をレスポンスとして返す統合方式。シンプルで現代的なベストプラクティス。
payload_format_version
Lambda Proxy 統合の event 構造のバージョン。HTTP API は 2.0、REST API は 1.0。新規構築は 2.0。
$default ステージ
HTTP API で URL がシンプルになる特殊なステージ名。https://xxx.execute-api.../ 形式(ステージ名がパスに入らない)。
Throttling
API Gateway の流量制御。burst_limit(一時的な瞬発上限)と rate_limit(持続的な秒間上限)。
Lambda
AWS のサーバレス関数実行サービス。アクセス無い時は完全無料、リクエスト発生時のみ実行課金。
runtime (Lambda)
Lambda が走らせる言語ランタイム。nodejs20.x / python3.12 / java21 等。
arm64 / Graviton
AWS の ARM ベースプロセッサ。Lambda で arm64 を選ぶと x86_64 より約 20% 安く、性能ほぼ同等。
memory_size (Lambda)
Lambda の割当メモリ。CPU はメモリに比例。256 MB が無料枠との兼ね合いで標準的。
timeout (Lambda)
Lambda の最大実行時間。デフォルト 3 秒、最大 15 分。フォーム処理なら 10 秒で十分。
archive_file (Terraform data source)
ディレクトリを zip にまとめる Terraform data source。Lambda コードのパッケージング用。
source_code_hash
Lambda コードの SHA256 ハッシュ。これが変わると Lambda が更新される。
aws_lambda_permission
Lambda のリソースベースポリシー。「誰がこの Lambda を InvokeFunction できるか」を定義。API Gateway から呼ぶには必須。
execution_arn (API Gateway)
Lambda Permission の source_arn に使う API Gateway の ARN。${execution_arn}/*/* でステージ・メソッドを wildcard 指定。
CloudWatch Log Group
CloudWatch Logs のログ集約単位。Lambda 関数 1 個に 1 個の log group が紐付く。/aws/lambda/<関数名> の名前が慣例。
retention_in_days
CloudWatch Log Group の保持日数。明示しないと無期限保持になりコスト膨張する。14 日 / 30 日が定番。
environment.variables (Lambda)
Lambda に渡す環境変数。コード内で process.env.NAME で読める。
CORS preflight
ブラウザがクロスオリジン POST 等をする前に送る OPTIONS リクエスト。「この origin からこのメソッドで呼んでいいか」を確認する。