★★ 中級

07. Lambda / API Gateway

サーバを管理せずに HTTP API を作る最小構成。Lambda(実行)API Gateway HTTP API(HTTP 受付)IAM ロール(権限)の 3 点セット。

リクエスト処理の流れ

クライアント → API Gateway → Lambda → DynamoDB/S3/RDS の処理フロー
API Gateway が AWS_PROXY 統合で Lambda を呼び出し、Lambda は IAM Role の権限で各種 AWS リソースを操作する。

料金は 呼ばれた回数 × 実行時間 のみ。リクエストが来ない夜中は $0。

関数コードの zip 化

関数のコードは zip にして Lambda に渡します。Terraform の archive_file で生成可。

# archive_file は terraform プロバイダ標準のデータソース
# source_dir のファイル群を zip にまとめて output_path に置く
data "archive_file" "hello" {
  type        = "zip"
  source_dir  = "${path.module}/lambda/hello"      # ソース(このディレクトリ配下を全部)
  output_path = "${path.module}/build/hello.zip"   # ビルド先(gitignore 推奨)
}

例: lambda/hello/index.js

exports.handler = async (event) => {
  return {
    statusCode: 200,
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      message: "Hello from Lambda!",
      path: event.rawPath,
    }),
  };
};

Lambda 用 IAM ロール

# Trust Policy(誰がこのロールを引き受けられるか)を policy_document で組み立て
# Lambda サービス自身がこのロールを使えるようにする宣言
data "aws_iam_policy_document" "lambda_assume" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"                       # AWS のサービス
      identifiers = ["lambda.amazonaws.com"]        # Lambda サービス
    }
  }
}

# Lambda 実行ロール本体(中身の権限ポリシーは attachment で別途貼る)
resource "aws_iam_role" "lambda" {
  name               = "lambda-hello"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

# CloudWatch Logs に書く最小権限
# AWS マネージドポリシー(logs:CreateLogGroup/Stream/PutLogEvents)を割り当て
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

aws_lambda_function

# Lambda 関数本体
resource "aws_lambda_function" "hello" {
  function_name = "hello"                          # AWS 上での関数名
  role          = aws_iam_role.lambda.arn          # 実行ロール(必須)

  # zip ファイルのパス + ハッシュ。ハッシュが変わると Terraform が再デプロイを検知
  filename         = data.archive_file.hello.output_path
  source_code_hash = data.archive_file.hello.output_base64sha256

  runtime = "nodejs20.x"      # 実行ランタイム(python3.12, java21 等)
  handler = "index.handler"   # ファイル名.関数名(index.js の handler を呼ぶ)

  memory_size = 256       # MB(128〜10240)。CPU 性能はメモリに比例
  timeout     = 10        # 秒(最大 900 = 15 分)
  architectures = ["arm64"]   # Graviton(同性能で 20% 安い)

  # 環境変数:コード側で process.env.LOG_LEVEL のように参照
  environment {
    variables = {
      LOG_LEVEL = "info"
    }
  }
}

Lambda の主要属性

属性デフォルト用途
memory_size128大きいほど CPU 性能も上がる
timeout3 秒最大 15 分
architecturesx86_64arm64 推奨(Graviton で割安)
vpc_config無しVPC 内のリソースにアクセスする時のみ
reserved_concurrent_executions無し同時実行数の上限
layers無し共通ライブラリの分離(ARN 指定)

aws_apigatewayv2_api(HTTP API)

API Gateway には REST API(v1)と HTTP API(v2)がありますが、新規は HTTP API(v2)が標準。料金が約 70% 安く、構成もシンプル。

# HTTP API(v2)の入れ物。protocol_type で v2 種別が決まる(HTTP / WEBSOCKET)
resource "aws_apigatewayv2_api" "main" {
  name          = "hello-api"
  protocol_type = "HTTP"

  # CORS:ブラウザから別オリジンの API を叩く時に必須
  cors_configuration {
    allow_origins = ["*"]                          # 本番は具体的ドメインに絞る
    allow_methods = ["GET", "POST"]
    allow_headers = ["Content-Type", "Authorization"]
    max_age       = 86400                          # プリフライト結果のキャッシュ(秒)
  }
}

integration / route / stage

# Integration: 「API → Lambda」の接続定義
# AWS_PROXY は「リクエストをそのまま Lambda に渡す」モード(一番よく使う)
resource "aws_apigatewayv2_integration" "hello" {
  api_id           = aws_apigatewayv2_api.main.id
  integration_type = "AWS_PROXY"
  integration_uri  = aws_lambda_function.hello.invoke_arn   # 呼び出し用 ARN

  payload_format_version = "2.0"      # v2 推奨(event のフォーマットが新)
  timeout_milliseconds   = 10000      # APIGW 側の最大 30000
}

# Route: 「GET /hello → 上の integration」
# route_key の形式: "メソッド パス"。"$default" で全パスをキャッチも可能
resource "aws_apigatewayv2_route" "hello" {
  api_id    = aws_apigatewayv2_api.main.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.hello.id}"
}

# Stage: 公開環境
# HTTP API では $default ステージを使うと URL に /stage/ が入らずシンプルになる
resource "aws_apigatewayv2_stage" "default" {
  api_id      = aws_apigatewayv2_api.main.id
  name        = "$default"
  auto_deploy = true                  # route 変更で自動デプロイ

  # アクセスログ出力先と JSON フォーマット
  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.api.arn
    # $context.* は APIGW が提供する変数。jsonencode で構造化ログに
    format = jsonencode({
      requestId      = "$context.requestId"
      sourceIp       = "$context.identity.sourceIp"
      requestTime    = "$context.requestTime"
      httpMethod     = "$context.httpMethod"
      routeKey       = "$context.routeKey"
      status         = "$context.status"
      responseLength = "$context.responseLength"
    })
  }
}

# アクセスログ用の Log Group
resource "aws_cloudwatch_log_group" "api" {
  name              = "/aws/apigw/hello-api"
  retention_in_days = 14   # コスト管理のため必ず設定(未設定だと永久保持)
}

aws_lambda_permission

API Gateway が Lambda を呼ぶ権限を 明示 する必要があります。これを忘れると 500 エラーで詰まります。

# Lambda 側のリソースベースポリシー
# 「APIGW がこの関数を呼んで OK」と Lambda 自身に許可させる(IAM ロールとは別レイヤー)
resource "aws_lambda_permission" "apigw" {
  statement_id  = "AllowAPIGatewayInvoke"           # ポリシー内のステートメント識別子
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.hello.function_name
  principal     = "apigateway.amazonaws.com"         # APIGW サービス

  # この API & 任意のステージ/メソッド/リソース から
  # /*/*  = どのステージ/どのメソッド・パスからでも OK
  source_arn = "${aws_apigatewayv2_api.main.execution_arn}/*/*"
}

完成形と動作確認

# API のエンドポイント URL を出力
# apply 後に terraform output -raw api_url で取得できる
output "api_url" {
  value = aws_apigatewayv2_stage.default.invoke_url
}
$ terraform apply
$ curl "$(terraform output -raw api_url)/hello"
{"message":"Hello from Lambda!","path":"/hello"}