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?

【Terraform×AWS】LambdaとAPI Gateway(HTTP API)でサーバーレスAPIを構築する

0
Posted at

小さなAPIやWebhookの受け口を作るとき、サーバーやコンテナを用意するほどでもない、という場面はよくあります。LambdaとAPI GatewayのHTTP APIを組み合わせれば、リクエストがあったときだけ動き、使った分だけ料金がかかるAPIを作れます。

この記事では、LambdaとAPI Gateway(HTTP API)のサーバーレスAPIをTerraformで構築します。あわせて、Terraformで管理するときにつまずきやすい点も紹介します。

構成

  • Lambda関数:リクエストを受けて処理する。ここではNode.jsで書く
  • API Gateway(HTTP API):HTTPSのエンドポイントを提供し、リクエストをLambdaに渡す
  • IAMロール:Lambdaが実行時に使う権限
  • CloudWatch Logs:Lambdaのログの保存先

API Gatewayには「REST API」と「HTTP API」の2種類があります。HTTP APIは機能を絞ったぶん、**料金が安く、設定も少なくて済みます。**APIキーによる利用制限やリクエストの細かい変換が必要なければ、HTTP APIで十分です。

Lambda関数のコード

src/index.mjs に、次のコードを置きます。

export const handler = async (event) => {
  const name = event.queryStringParameters?.name ?? "world";

  return {
    statusCode: 200,
    headers: { "content-type": "application/json" },
    body: JSON.stringify({ message: `hello, ${name}` }),
  };
};

Lambda関数とIAMロール

コードをzipにまとめ、Lambda関数を作ります。

variable "function_name" {
  type    = string
  default = "hello-api"
}

data "archive_file" "lambda" {
  type        = "zip"
  source_dir  = "${path.module}/src"
  output_path = "${path.module}/build/lambda.zip"
}

resource "aws_iam_role" "lambda" {
  name = "${var.function_name}-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "lambda.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${var.function_name}"
  retention_in_days = 14
}

resource "aws_lambda_function" "api" {
  function_name    = var.function_name
  role             = aws_iam_role.lambda.arn
  runtime          = "nodejs22.x"
  handler          = "index.handler"
  architectures    = ["arm64"]
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout          = 10
  memory_size      = 256

  depends_on = [aws_cloudwatch_log_group.lambda]
}

ポイントは次のとおりです。

  • source_code_hash:zipのハッシュ値を渡す。これがないと、コードを変えてもTerraformが変更に気づかず、Lambdaが更新されない
  • ロググループを先に作る:Lambdaは初回実行時にロググループを自動で作るが、その場合は保存期間が無期限になる。Terraformで先に作り、retention_in_daysで保存期間を決めておく。depends_onで作成順を保証している
  • architectures = ["arm64"]:Graviton(ARM)で動かす。x86_64より料金が安い。ネイティブのライブラリを使わないコードなら、そのまま動く

API Gateway(HTTP API)

resource "aws_apigatewayv2_api" "http" {
  name          = "${var.function_name}-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "lambda" {
  api_id                 = aws_apigatewayv2_api.http.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.api.invoke_arn
  payload_format_version = "2.0"
}

resource "aws_apigatewayv2_route" "hello" {
  api_id    = aws_apigatewayv2_api.http.id
  route_key = "GET /hello"
  target    = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}

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

  default_route_settings {
    throttling_rate_limit  = 50
    throttling_burst_limit = 100
  }
}

output "endpoint" {
  value = "${aws_apigatewayv2_stage.default.invoke_url}hello"
}
  • integration:API GatewayとLambdaをつなぐ設定。AWS_PROXYにすると、リクエストの内容がそのままLambdaに渡る
  • route:「GET /hello」のように、HTTPメソッドとパスの組み合わせごとに、どのintegrationに渡すかを決める
  • stage:$default という名前にすると、URLにステージ名が入らない。auto_deployで、変更が自動で反映される
  • default_route_settings:1秒あたりのリクエスト数の上限。想定外の大量アクセスで料金が膨らむのを防ぐ

API GatewayからLambdaを呼ぶ許可

最後に、API GatewayがLambdaを呼び出せるように許可します。これを忘れると、APIを呼んでも500エラーになります。

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

source_arnで「このAPIからの呼び出しだけ」に限定しています。限定しないと、他のAPI Gatewayからも呼べる状態になります。

動作確認

applyすると、outputにエンドポイントのURLが表示されます。

terraform apply
curl "$(terraform output -raw endpoint)?name=terraform"

次のようなJSONが返れば成功です。

{"message":"hello, terraform"}

コードの更新をTerraformと分けるか

この記事の構成では、Lambdaのコードを変えるたびにterraform applyで更新します。小さなAPIならこれで十分です。

一方、コードの変更が多い場合は、インフラはTerraform、コードのデプロイはCI/CDと分けることもあります。その場合は、CIがLambdaのコードを更新したあとにTerraformが古いコードへ戻さないよう、aws_lambda_functionのlifecycleでsource_code_hashなどの差分を無視させます。どちらの方式にするかは、チームでコードを更新する頻度に合わせて決めてください。

まとめ

  • 小さなAPIなら、LambdaとAPI GatewayのHTTP APIで、使った分だけ料金がかかる構成にできる
  • source_code_hashを指定しないと、コードの変更がLambdaに反映されない
  • ロググループはTerraformで先に作り、保存期間を決めておく
  • aws_lambda_permissionを忘れると、APIを呼んでも500エラーになる
  • スロットリングを設定し、想定外のアクセスで料金が膨らむのを防ぐ

サーバーを持たない構成でも、権限、ログ、上限値の設定は必要です。最初からTerraformで管理しておくと、設定漏れに気づきやすくなります。

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?