REST API Gateway with Lambda Integration and Stage Deployment
Scenario
You have a Python Lambda function that returns a JSON greeting response and you want to expose it as a public HTTP endpoint. API Gateway REST API is the managed API layer that sits in front of Lambda — it handles routing, authentication, rate limiting, and SSL termination so you do not have to run a web server.
Create a REST API with a single GET /hello endpoint that triggers a Lambda function packaged from local Python source code, then deploy it to a named stage with throttling configured.
- Write a Python handler (lambda_src/handler.py) that reads a name query parameter and returns a JSON greeting with statusCode 200.
- Use the archive_file data source to package the source directory into a zip and deploy it as an aws_lambda_function with its own IAM execution role.
- Create an aws_api_gateway_rest_api with a /hello resource and a GET method using AWS_PROXY Lambda integration.
- Grant lambda:InvokeFunction to API Gateway via aws_lambda_permission.
- Create an aws_api_gateway_deployment with a hash-based trigger and a prod stage with throttling enabled.
- Output the full invoke URL for the /hello endpoint.
Additional Context
In REST API Gateway with Terraform, a deployment must be recreated whenever the API configuration changes (new routes, methods, integrations). A common pattern is to add a triggers argument to aws_api_gateway_deployment containing a hash of all related resources — this forces a new deployment on every change.
The AWS_PROXY integration type passes the entire HTTP request to Lambda as an event — headers, body, query params, path, etc. Your Lambda handler must return a specific JSON shape with statusCode and body. Without this shape, API Gateway returns a 502 Bad Gateway.
lambda_src/handler.py
# lambda_src/handler.py (Python source — not HCL)
import json
def lambda_handler(event, context):
# API Gateway proxy passes query string params under queryStringParameters
params = event.get("queryStringParameters") or {}
name = params.get("name", "World")
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"message": f"Hello, {name}!"})
}versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
archive = { source = "hashicorp/archive", version = "~> 2.4" }
}
}variables.tf
variable "aws_region" { type = string; default = "us-east-1" }
variable "api_name" { type = string; default = "hello-api" }
variable "environment" { type = string; default = "prod" }main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
# Package the lambda_src/ directory into a zip file.
# Terraform re-zips on every plan. If source code changes, source_code_hash changes
# and Terraform triggers a Lambda update automatically.
data "archive_file" "lambda_zip" {
type = "zip"
source_dir = "${path.module}/lambda_src"
output_path = "${path.module}/lambda.zip"
}
# Trust policy for Lambda.
data "aws_iam_policy_document" "lambda_assume" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals { type = "Service"; identifiers = ["lambda.amazonaws.com"] }
}
}
# Lambda execution role — allows writing logs to CloudWatch.
resource "aws_iam_role" "lambda" {
name = "${var.api_name}-lambda-role"
assume_role_policy = data.aws_iam_policy_document.lambda_assume.json
}
resource "aws_iam_role_policy_attachment" "lambda_logs" {
role = aws_iam_role.lambda.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
# Lambda function — packages Python code from the local lambda_src/ directory.
resource "aws_lambda_function" "hello" {
function_name = "${var.api_name}-handler"
runtime = "python3.12"
handler = "handler.lambda_handler" # filename.function_name
role = aws_iam_role.lambda.arn
filename = data.archive_file.lambda_zip.output_path
source_code_hash = data.archive_file.lambda_zip.output_base64sha256
depends_on = [aws_iam_role_policy_attachment.lambda_logs]
}
# REST API.
resource "aws_api_gateway_rest_api" "main" {
name = var.api_name
description = "Hello World REST API backed by Lambda"
endpoint_configuration { types = ["REGIONAL"] }
}
# Resource — the /hello path.
resource "aws_api_gateway_resource" "hello" {
rest_api_id = aws_api_gateway_rest_api.main.id
parent_id = aws_api_gateway_rest_api.main.root_resource_id
path_part = "hello"
}
# Method — GET /hello with no authentication (for this lab).
resource "aws_api_gateway_method" "get_hello" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.hello.id
http_method = "GET"
authorization = "NONE"
}
# Lambda proxy integration — passes the entire HTTP request to Lambda.
# The integration HTTP method for Lambda is always POST internally.
resource "aws_api_gateway_integration" "lambda" {
rest_api_id = aws_api_gateway_rest_api.main.id
resource_id = aws_api_gateway_resource.hello.id
http_method = aws_api_gateway_method.get_hello.http_method
integration_http_method = "POST"
type = "AWS_PROXY"
uri = aws_lambda_function.hello.invoke_arn
}
# Permission — API Gateway must be granted explicit permission to invoke Lambda.
# Without this, Lambda rejects API Gateway invocations with a 403.
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_api_gateway_rest_api.main.execution_arn}/*/*"
}
# Deployment — a snapshot of the API config at a point in time.
# triggers forces a new deployment when the integration or method changes.
resource "aws_api_gateway_deployment" "main" {
rest_api_id = aws_api_gateway_rest_api.main.id
triggers = {
redeployment = sha1(jsonencode([
aws_api_gateway_resource.hello.id,
aws_api_gateway_method.get_hello.id,
aws_api_gateway_integration.lambda.id,
]))
}
lifecycle {
create_before_destroy = true
}
depends_on = [aws_api_gateway_integration.lambda]
}
# Stage — the callable version of the API (e.g., "prod").
resource "aws_api_gateway_stage" "prod" {
deployment_id = aws_api_gateway_deployment.main.id
rest_api_id = aws_api_gateway_rest_api.main.id
stage_name = var.environment
default_route_settings {
throttling_rate_limit = 100
throttling_burst_limit = 200
}
}outputs.tf
output "invoke_url" {
description = "Full URL to call the /hello endpoint."
value = "${aws_api_gateway_stage.prod.invoke_url}/hello"
}Workflow Commands
mkdir -p lambda_src
# Create lambda_src/handler.py with the Python code shown above
terraform init
terraform apply
# Call the API
curl "$(terraform output -raw invoke_url)?name=Terraform"
# {"message": "Hello, Terraform!"}
terraform destroyApply complete! Resources: 9 added, 0 changed, 0 destroyed. Outputs: invoke_url = "https://abc1234def.execute-api.us-east-1.amazonaws.com/prod/hello"

