小さな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で管理しておくと、設定漏れに気づきやすくなります。