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:

  1. The aws_api_gateway_deployment Resource: This is the linchpin. Without it, your API Gateway configuration exists in an "edit" mode, not a "live" mode. The triggers argument 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.

  2. Integration Details: The aws_api_gateway_integration resource requires precise configuration. For Lambda integrations, type = "AWS_PROXY" is standard. The uri needs to point to the specific Lambda function ARN, often constructed dynamically. The integration_http_method is often overlooked; for AWS_PROXY integrations, it should typically be POST, 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_PROXY for Lambda, ensure type = "AWS_PROXY" and integration_http_method = "POST" in your aws_api_gateway_integration resource. If proxying to another HTTP endpoint, use type = "HTTP_PROXY" and ensure integration_http_method matches the target endpoint’s expected method.
    • Why it works: API Gateway needs to know how to talk to the backend. AWS_PROXY expects a specific payload format and uses POST to invoke Lambda, while other integration types might use different HTTP methods and payload transformations.
  3. 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_response and aws_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_response and aws_api_gateway_integration_response resources 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.
  4. Resource Path and Parent ID: Ensure that the parent_id in your aws_api_gateway_resource correctly points to the root_resource_id or 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_id for top-level resources or parent_id = aws_api_gateway_resource.parent_resource.id for nested resources.
    • Why it works: API Gateway resources form a hierarchical tree. Each resource must have a parent, ultimately tracing back to the root.
  5. Permissions (IAM Role): If your integration involves AWS services like Lambda, the API Gateway service principal needs permission to invoke those services. The credentials argument in aws_api_gateway_integration usually 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 credentials has a policy granting lambda: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.
  6. 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 Forbidden or 429 Too Many Requests if keys are missing or plans are exceeded.
    • Fix: Ensure aws_api_gateway_api_key and aws_api_gateway_usage_plan resources are created and associated with your aws_api_gateway_stage using aws_api_gateway_usage_plan_key.
    • Why it works: These are separate security and traffic management layers that must be explicitly enabled and configured.

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.

Want structured learning?

Take the full Apigateway course →