Terraform, the infrastructure-as-code darling, can feel like a magic wand for provisioning cloud resources, but when it comes to API Gateways, the magic can get a little… opaque. The real surprise is that your Terraform code might be perfectly valid, and the API Gateway resource might even appear provisioned, yet your API won’t respond because the core issue lies with how Terraform describes the API’s endpoints to the gateway, not with the gateway itself.
Let’s see this in action. Imagine you have a simple AWS API Gateway REST API that’s supposed to proxy requests to a Lambda function. Your Terraform might look something like this:
resource "aws_api_gateway_rest_api" "my_api" {
name = "MyAwesomeAPI"
description = "A simple API for demonstration"
}
resource "aws_api_gateway_resource" "my_resource" {
rest_api_id = aws_api_gateway_rest_api.my_api.id
parent_id = aws_api_gateway_rest_api.my_api.root_resource_id
path_part = "hello"
}
resource "aws_api_gateway_method" "hello_get" {
rest_api_id = aws_api_gateway_rest_api.my_api.id
resource_id = aws_api_gateway_resource.my_resource.id
http_method = "GET"
authorization = "NONE"
}
resource "aws_api_gateway_integration" "hello_get_integration" {
rest_api_id = aws_api_gateway_rest_api.my_api.id
resource_id = aws_api_gateway_resource.my_resource.id
http_method = aws_api_gateway_method.hello_get.http_method
type = "AWS_PROXY"
integration_http_method = "POST" # Often overlooked, this is key
uri = "arn:aws:apigateway:${var.aws_region}.amazonaws.com/lambda/invoke" # This URI is a placeholder for the actual integration target
credentials = aws_iam_role.apigateway_lambda_exec_role.arn # Assuming you have this role defined
passthrough_behavior = "WHEN_NO_TEMPLATES"
}
resource "aws_api_gateway_deployment" "my_deployment" {
rest_api_id = aws_api_gateway_rest_api.my_api.id
triggers = {
redeployment = sha1(jsonencode(aws_api_gateway_resource.my_resource.path_part)) # A simple trigger
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_api_gateway_stage" "my_stage" {
deployment_id = aws_api_gateway_deployment.my_deployment.id
rest_api_id = aws_api_gateway_rest_api.my_api.id
stage_name = "dev"
}
This looks complete, right? You’ve defined the API, a resource (/hello), a method (GET), an integration, and a deployment. But if you try to call your-api-id.execute-api.your-region.amazonaws.com/dev/hello, you’ll likely get a 403 Forbidden or a 500 Internal Server Error.
The core problem is that API Gateway is a state machine. Every time you make a change to your API definition (resources, methods, integrations), you need to deploy that change. Terraform handles the creation and updates of the API Gateway resources themselves, but it doesn’t automatically trigger a deployment unless you explicitly tell it to. The aws_api_gateway_deployment resource is your tool for this.
Here’s what you need to nail down:
-
The
aws_api_gateway_deploymentResource: This is the linchpin. Without it, your API Gateway configuration exists in an "edit" mode, not a "live" mode. Thetriggersargument is crucial here. It tells Terraform when to create a new deployment. A common pattern is to use a hash of the API definition.sha1(jsonencode(aws_api_gateway_resource.my_resource.path_part))is a simplistic example; a more robust approach would hash the entire relevant API definition. When any part of that definition changes, the hash changes, triggering a new deployment. -
Integration Details: The
aws_api_gateway_integrationresource requires precise configuration. For Lambda integrations,type = "AWS_PROXY"is standard. Theurineeds to point to the specific Lambda function ARN, often constructed dynamically. Theintegration_http_methodis often overlooked; forAWS_PROXYintegrations, it should typically bePOST, as API Gateway sends a POST request to the Lambda endpoint.- Diagnosis: Check the API Gateway console’s "Integration Request" for your method. Ensure the "Lambda Function" or "HTTP Endpoint" is correctly specified.
- Fix: If using
AWS_PROXYfor Lambda, ensuretype = "AWS_PROXY"andintegration_http_method = "POST"in youraws_api_gateway_integrationresource. If proxying to another HTTP endpoint, usetype = "HTTP_PROXY"and ensureintegration_http_methodmatches the target endpoint’s expected method. - Why it works: API Gateway needs to know how to talk to the backend.
AWS_PROXYexpects a specific payload format and usesPOSTto invoke Lambda, while other integration types might use different HTTP methods and payload transformations.
-
Method Request and Response: While not explicitly in the example, ensure your "Method Request" and "Method Response" in the API Gateway console (or Terraform equivalents like
aws_api_gateway_method_responseandaws_api_gateway_integration_response) are configured correctly, especially for handling different status codes and response bodies.- Diagnosis: In the API Gateway console, under your method, check "Method Response" for expected status codes (e.g., 200) and "Integration Response" for mapping responses from your backend to API Gateway responses.
- Fix: Define
aws_api_gateway_method_responseandaws_api_gateway_integration_responseresources in Terraform to explicitly map backend responses (like Lambda output) to API Gateway’s expected responses. - Why it works: This bridges the gap between what your backend returns and what the API Gateway client expects. Without proper mapping, even a successful backend execution might result in an error at the gateway level.
-
Resource Path and Parent ID: Ensure that the
parent_idin youraws_api_gateway_resourcecorrectly points to theroot_resource_idor another existing resource. A mismatch here means the resource isn’t attached to the API structure.- Diagnosis: Terraform plan might show the resource being created but not associated correctly, or you’ll see a 404 from the gateway.
- Fix: Double-check that
parent_id = aws_api_gateway_rest_api.my_api.root_resource_idfor top-level resources orparent_id = aws_api_gateway_resource.parent_resource.idfor nested resources. - Why it works: API Gateway resources form a hierarchical tree. Each resource must have a parent, ultimately tracing back to the root.
-
Permissions (IAM Role): If your integration involves AWS services like Lambda, the API Gateway service principal needs permission to invoke those services. The
credentialsargument inaws_api_gateway_integrationusually points to an IAM role that grants these permissions.- Diagnosis: 403 Forbidden errors often stem from missing IAM permissions. Check CloudTrail logs for "AccessDenied" errors related to API Gateway trying to invoke your backend.
- Fix: Ensure the IAM role specified in
credentialshas a policy grantinglambda:InvokeFunction(or similar for other services) for your specific backend resource. - Why it works: AWS security is based on IAM. API Gateway, as an AWS service, must be explicitly authorized to call other AWS services on your behalf.
-
API Key and Usage Plans (Optional but Common): If you’ve configured API keys and usage plans for throttling and security, ensure they are correctly linked to your stage.
- Diagnosis: Requests might be rejected with
403 Forbiddenor429 Too Many Requestsif keys are missing or plans are exceeded. - Fix: Ensure
aws_api_gateway_api_keyandaws_api_gateway_usage_planresources are created and associated with youraws_api_gateway_stageusingaws_api_gateway_usage_plan_key. - Why it works: These are separate security and traffic management layers that must be explicitly enabled and configured.
- Diagnosis: Requests might be rejected with
Once all these are correct and Terraform applies them, your API should be reachable. The next thing you’ll likely run into is configuring custom domains, which introduces DNS, ACM certificates, and mapping configurations.