AWS Lambda and API Gateway with Terraform: Serverless HTTP API

· 1 min read · Terraform & OpenTofu Tutorials

A serverless API in one Terraform configuration #

AWS Lambda runs code without servers, and API Gateway exposes it over HTTPS. This tutorial uses an HTTP API (API Gateway v2), which is simpler and cheaper than the REST API for most cases.

The function code #

src/handler.py
import json

def handler(event, context):
    name = (event.get("queryStringParameters") or {}).get("name", "world")
    return {
        "statusCode": 200,
        "headers": {"Content-Type": "application/json"},
        "body": json.dumps({"message": f"Hello, {name}!"}),
    }

Package the code #

lambda.tf
data "archive_file" "lambda" {
  type        = "zip"
  source_file = "${path.module}/src/handler.py"
  output_path = "${path.module}/build/handler.zip"
}

The archive provider creates the zip during the plan. For larger projects with dependencies, build the package in CI and upload it to S3, or use a container image from ECR.

IAM role and logs #

lambda.tf
data "aws_iam_policy_document" "lambda_assume" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "lambda" {
  name               = "ditwl-hello-lambda"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}

resource "aws_iam_role_policy_attachment" "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/ditwl-hello"
  retention_in_days = 14
}

Creating the log group yourself lets you set a retention and avoids logs that are kept forever. See IAM roles.

The function #

lambda.tf
resource "aws_lambda_function" "hello" {
  function_name    = "ditwl-hello"
  role             = aws_iam_role.lambda.arn
  runtime          = "python3.12"
  handler          = "handler.handler"
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  timeout          = 10
  memory_size      = 128
  architectures    = ["arm64"]

  environment {
    variables = {
      LOG_LEVEL = "info"
    }
  }

  depends_on = [
    aws_iam_role_policy_attachment.logs,
    aws_cloudwatch_log_group.lambda,
  ]
}

source_code_hash makes Terraform redeploy when the code changes. Use a supported runtime: AWS deprecates old ones, so check the list. arm64 (Graviton) is cheaper.

API Gateway HTTP API #

api.tf
resource "aws_apigatewayv2_api" "http" {
  name          = "ditwl-hello-api"
  protocol_type = "HTTP"
}

resource "aws_apigatewayv2_integration" "hello" {
  api_id                 = aws_apigatewayv2_api.http.id
  integration_type       = "AWS_PROXY"
  integration_uri        = aws_lambda_function.hello.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.hello.id}"
}

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

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

output "api_url" {
  value = aws_apigatewayv2_api.http.api_endpoint
}

The aws_lambda_permission is the piece people forget: without it, API Gateway receives 500 Internal Server Error because it cannot invoke the function.

Deploy and test #

$ tofu init
$ tofu apply
$ curl "$(tofu output -raw api_url)/hello?name=Terraform"
{"message": "Hello, Terraform!"}

Next steps #

Cost #

Lambda charges for requests and execution time, and HTTP API charges per million requests. Both have a monthly free tier for Lambda, and a small API costs cents. Delete the stack when finished with tofu destroy.

#AWS #Serverless #Terraform #OpenTofu