Provisions a complete Amazon API Gateway V1 REST API β resource tree, methods, integrations, authorizers, models, deployment, stages, custom domains, usage plans/API keys, VPC Links, and gateway responses β from one module call, REGIONAL and X-Ray-traced by default. Built for the AWS provider v6.x.
- π³ One REST API, fully wired. Creates
aws_api_gateway_rest_apiplus the entire self-referential resource tree, methods, integrations, method/integration responses, authorizers, models, request validators, a deployment, stages, custom domains, usage plans, API keys, VPC Links, gateway responses, client certificates, and documentation artifacts. - π REGIONAL by default, not EDGE.
endpoint_configuration.typesdefaults to["REGIONAL"]β the baseline β overriding AWS's own implicit["EDGE"]default. Choose["EDGE"]or["PRIVATE"]deliberately. - π« Authorization is never silently
"NONE". Everymethods[*]entry requires an explicitauthorizationvalue β there is no default to fall back on, so an open method is always a conscious choice, not an accident. - π©» X-Ray tracing on by default.
stages[*].xray_tracing_enableddefaults totrueβ the observability baseline for a regulated FI. - π No accidental PII in logs.
method_settings[*].settings.data_trace_enableddefaults tofalseβ full request/response body logging can leak PII into CloudWatch Logs, so it is opt-in per method. - π² Self-referential resource tree.
resources[*].parent_keycan reference any other key in the same map β Terraform resolves the full path hierarchy from a flat, caller-supplied map with no manual ordering. - π Hash-triggered redeployment. The module's single
aws_api_gateway_deploymentautomatically redeploys whenever the resource/method/integration/authorizer/model/gateway-response maps change shape, via asha1-hashedtriggersmap plus explicitdepends_on(per the provider's own guidance) β withcreate_before_destroyalways on, so recreation never collides with an active stage. - π Metering built in. Usage plans, API keys, and usage-plan-key associations are first-class
map(object(...))inputs, keyed and cross-referenced automatically. - πΈοΈ Private integrations supported.
aws_api_gateway_vpc_linkbridges to an internal NLB (tf-mod-aws-lb) forHTTP/HTTP_PROXYintegrations without exposing the backend publicly. - π§ One authoring style, on purpose. This module manages the API exclusively via the native Terraform resource graph β it does not expose the OpenAPI-body import path (
aws_api_gateway_rest_api_put/ thebodyargument), because the provider's own docs warn that mixing the two silently overwrites Terraform-managed child resources. - π·οΈ Tags everywhere taggable.
var.tagsmerges with providerdefault_tagsand flows to the REST API, API keys, client certificates, domain names, usage plans, VPC Links, and stages;tags_allis surfaced on the REST API.
π‘ Why it matters: A REST API is a front door with dozens of moving parts β path tree, auth, backend wiring, deployment lifecycle, custom domain, metering. A single secure-by-default module keeps REGIONAL endpoints, explicit authorization, X-Ray tracing, and PII-safe logging consistent from the first
terraform apply, instead of relying on every author to remember two dozen individual settings.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- β Star this repository to help others discover this Terraform module.
- π€ Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
tf-mod-aws-api-gateway is the V1 REST API counterpart to tf-mod-aws-api-gateway-v2 (HTTP/WebSocket) β it consumes identity, backend, certificate, and networking inputs from upstream siblings, and is itself consumed by DNS and WAF modules downstream.
flowchart LR
IAM[tf-mod-aws-iam-role] --> APIGW
LAMBDA[tf-mod-aws-lambda] --> APIGW
COGNITO[tf-mod-aws-cognito] --> APIGW
ACM[tf-mod-aws-acm] --> APIGW
VPCE[tf-mod-aws-vpc-endpoint] --> APIGW
LB[tf-mod-aws-lb NLB] --> APIGW
CWLG[tf-mod-aws-cloudwatch-log-group] --> APIGW
WAFV2[tf-mod-aws-wafv2] -. associates stage.-> APIGW
APIGW[tf-mod-aws-api-gateway REST v1] --> ROUTE53[tf-mod-aws-route53-zone]
APIGWV2[tf-mod-aws-api-gateway-v2 HTTP or WebSocket] -. sibling module.- APIGW
style APIGW fill:#FF9900,color:#fff
flowchart TD
RA[aws_api_gateway_rest_api.this]
RA --> RP[aws_api_gateway_rest_api_policy.this]
RA --> RES["aws_api_gateway_resource.this (for_each)"]
RES --> MET["aws_api_gateway_method.this (for_each)"]
MET --> MR[aws_api_gateway_method_response.this]
MET --> INT[aws_api_gateway_integration.this]
INT --> IR[aws_api_gateway_integration_response.this]
RA --> AUTH[aws_api_gateway_authorizer.this]
RA --> MODEL[aws_api_gateway_model.this]
RA --> RV[aws_api_gateway_request_validator.this]
RA --> GW[aws_api_gateway_gateway_response.this]
RA --> DEP[aws_api_gateway_deployment.this]
DEP --> STAGE["aws_api_gateway_stage.this (for_each)"]
STAGE --> MS[aws_api_gateway_method_settings.this]
CERT[aws_api_gateway_client_certificate.this] --> STAGE
RA --> DOMAIN[aws_api_gateway_domain_name.this]
DOMAIN --> DNAA[aws_api_gateway_domain_name_access_association.this]
DOMAIN --> BPM[aws_api_gateway_base_path_mapping.this]
STAGE --> BPM
RA --> UP[aws_api_gateway_usage_plan.this]
UP --> UPK[aws_api_gateway_usage_plan_key.this]
APIKEY[aws_api_gateway_api_key.this] --> UPK
VPCLINK[aws_api_gateway_vpc_link.this] --> INT
RA --> DOC[aws_api_gateway_documentation_part.this]
DOC --> DOCV[aws_api_gateway_documentation_version.this]
ACCT["aws_api_gateway_account.this (opt-in singleton)"]
style RA fill:#FF9900,color:#fff
| Resource | Count | Created when |
|---|---|---|
aws_api_gateway_rest_api.this |
1 | always (keystone) |
aws_api_gateway_rest_api_policy.this |
0..1 | var.policy != null |
aws_api_gateway_resource.this |
0..N | one per resources entry |
aws_api_gateway_model.this |
0..N | one per models entry |
aws_api_gateway_request_validator.this |
0..N | one per request_validators entry |
aws_api_gateway_authorizer.this |
0..N | one per authorizers entry |
aws_api_gateway_method.this |
0..N | one per methods entry |
aws_api_gateway_method_response.this |
0..N | one per method_responses entry |
aws_api_gateway_integration.this |
0..N | one per integrations entry |
aws_api_gateway_integration_response.this |
0..N | one per integration_responses entry |
aws_api_gateway_gateway_response.this |
0..N | one per gateway_responses entry |
aws_api_gateway_deployment.this |
1 | always (hash-triggered redeploy) |
aws_api_gateway_stage.this |
0..N | one per stages entry |
aws_api_gateway_method_settings.this |
0..N | one per method_settings entry |
aws_api_gateway_client_certificate.this |
0..N | one per client_certificates entry |
aws_api_gateway_domain_name.this |
0..N | one per domain_names entry |
aws_api_gateway_domain_name_access_association.this |
0..N | one per domain_name_access_associations entry |
aws_api_gateway_base_path_mapping.this |
0..N | one per base_path_mappings entry |
aws_api_gateway_usage_plan.this |
0..N | one per usage_plans entry |
aws_api_gateway_api_key.this |
0..N | one per api_keys entry |
aws_api_gateway_usage_plan_key.this |
0..N | one per usage_plan_keys entry |
aws_api_gateway_vpc_link.this |
0..N | one per vpc_links entry |
aws_api_gateway_documentation_part.this |
0..N | one per documentation_parts entry |
aws_api_gateway_documentation_version.this |
0..N | one per documentation_versions entry |
aws_api_gateway_account.this |
0..1 | var.manage_account_settings == true |
Every cross-reference between children is by key, not raw id/ARN β methods point at resources by resource_key, integrations point at methods by method_key, stages point at the single deployment automatically, usage plans point at stages by stage_key, and so on. The module resolves each key to the underlying resource attribute internally.
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
The module declares only a required_providers block (providers.tf) and inherits the single aws provider β there is no provider {} block, no region variable, and no credential variable. Credentials resolve through the standard AWS chain at the root/pipeline level. The caller chooses the Region by which provider configuration it passes into the aws slot.
β οΈ API Gateway V1 REST APIs are a REGIONAL/EDGE service managed from the Region the API lives in β there is no us-east-1 requirement for the module itself. The only us-east-1 touchpoint is indirect: an EDGE-optimized custom domain name's ACM certificate (domain_names[*].certificate_arn) must be requested in us-east-1 (API Gateway provisions an AWS-managed CloudFront distribution behind it), while a REGIONAL/PRIVATE domain's certificate (regional_certificate_arn) must be in the same Region as this module.
Least-privilege actions the Terraform execution identity needs to manage this module.
| Action | Required for | Notes |
|---|---|---|
apigateway:GET, apigateway:POST, apigateway:PUT, apigateway:PATCH, apigateway:DELETE on arn:aws:apigateway:*::/restapis* |
Full CRUD lifecycle of the REST API and every child resource | API Gateway authorizes by HTTP verb against resource-path ARNs, not per-resource-type action names |
apigateway:POST, apigateway:PATCH on /restapis/*/deployments*, /restapis/*/stages* |
Deployment and stage lifecycle | |
apigateway:POST on /usageplans*, /apikeys* |
Usage plan / API key lifecycle | |
apigateway:POST on /domainnames*, /domainnameaccessassociations*, /restapis/*/basepathmappings* |
Custom domain and base path mapping lifecycle | |
apigateway:POST on /vpclinks* |
VPC Link lifecycle | |
apigateway:PATCH on /account |
Account settings | Only when manage_account_settings = true |
iam:PassRole on authorizer / integration / account CloudWatch role ARNs |
Passing an IAM role to API Gateway | For Lambda authorizer invocation, AWS-service integrations, or account-level CloudWatch logging |
lambda:GetFunction, lambda:AddPermission |
Confirming/authorizing Lambda invocation | The companion aws_lambda_permission is created by the caller / tf-mod-aws-lambda, not this module |
acm:DescribeCertificate |
Resolving domain name certificates | |
ec2:DescribeVpcEndpoints |
Resolving PRIVATE endpoint VPC endpoint IDs | |
elasticloadbalancing:DescribeLoadBalancers |
Resolving VPC Link target NLB ARNs |
No service-linked role is auto-created for API Gateway.
- Account-level CloudWatch role is a Region-wide singleton.
aws_api_gateway_accountapplies to the whole account + Region, not this REST API. Setmanage_account_settings = truein exactly one module call per account/Region. - Custom domain certificates: EDGE domains need an ACM cert in us-east-1; REGIONAL/PRIVATE domains need one in this module's own Region.
- PRIVATE APIs need at least one
com.amazonaws.<region>.execute-apiinterface VPC endpoint (tf-mod-aws-vpc-endpoint) and a resource policy (var.policy) scopingexecute-api:Invoketo it β without both, the API is unreachable (safe failure) but non-functional. - Usage plans require their referenced stages to already exist β handled automatically by this module's internal resource references.
- Quotas: 600 REST APIs/account/Region (default); 300 resources/API; 10 authorizers/API; 60 req/s account-level throttle (soft); 500 usage plans/account β most raisable via Service Quotas.
tf-mod-aws-api-gateway/
βββ providers.tf
βββ variables.tf
βββ main.tf
βββ outputs.tf
βββ README.md
βββ SCOPE.md
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "core-orders-api"
description = "Orders REST API"
resources = {
orders = { path_part = "orders" }
}
methods = {
list_orders = {
resource_key = "orders"
http_method = "GET"
authorization = "AWS_IAM"
}
}
integrations = {
list_orders = {
method_key = "list_orders"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.orders_lambda.invoke_arn # tf-mod-aws-lambda
}
}
stages = {
prod = {
stage_name = "prod"
access_log_destination_arn = module.apigw_logs.arn # tf-mod-aws-cloudwatch-log-group
access_log_format = jsonencode({ requestId = "$context.requestId", ip = "$context.identity.sourceIp", status = "$context.status" })
}
}
tags = {
environment = "prod"
owner = "platform-team"
}
}| Input | Type | Source module |
|---|---|---|
endpoint_configuration.vpc_endpoint_ids |
list(string) |
tf-mod-aws-vpc-endpoint |
authorizers[*].authorizer_uri |
string (Lambda invoke ARN) |
tf-mod-aws-lambda |
authorizers[*].authorizer_credentials |
string (IAM role ARN) |
tf-mod-aws-iam-role |
authorizers[*].provider_arns |
list(string) (Cognito user pool ARNs) |
tf-mod-aws-cognito |
integrations[*].uri |
string (Lambda invoke ARN / URL / AWS URI) |
tf-mod-aws-lambda / external |
integrations[*].credentials |
string (IAM role ARN) |
tf-mod-aws-iam-role |
vpc_links[*].target_arns |
list(string) (NLB ARN) |
tf-mod-aws-lb |
domain_names[*].certificate_arn / .regional_certificate_arn |
string (ACM cert ARN) |
tf-mod-aws-acm |
domain_name_access_associations[*].access_association_source |
string (VPC endpoint id) |
tf-mod-aws-vpc-endpoint |
stages[*].access_log_destination_arn |
string (log group ARN) |
tf-mod-aws-cloudwatch-log-group |
account_cloudwatch_role_arn |
string (IAM role ARN) |
tf-mod-aws-iam-role |
See the SCOPE.md ## Emits table for the complete list β primary outputs id and arn, plus execution_arn (consumed by tf-mod-aws-lambda permissions), stage_invoke_urls, domain_name_regional_domain_names / domain_name_cloudfront_domain_names (consumed by tf-mod-aws-route53-zone), and api_key_values (sensitive).
1 Β· Minimal Lambda-proxy REST API
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "hello-api"
resources = {
hello = { path_part = "hello" }
}
methods = {
get_hello = {
resource_key = "hello"
http_method = "GET"
authorization = "NONE"
}
}
integrations = {
get_hello = {
method_key = "get_hello"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.hello_lambda.invoke_arn
}
}
stages = {
dev = { stage_name = "dev" }
}
}2 Β· Nested resource path with path parameters
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "orders-api"
resources = {
orders = { path_part = "orders" }
order_id = { parent_key = "orders", path_part = "{id}" }
fulfill = { parent_key = "order_id", path_part = "fulfill" }
}
methods = {
get_order = {
resource_key = "order_id"
http_method = "GET"
authorization = "AWS_IAM"
request_parameters = { "method.request.path.id" = true }
}
fulfill_order = {
resource_key = "fulfill"
http_method = "POST"
authorization = "AWS_IAM"
}
}
integrations = {
get_order = {
method_key = "get_order"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.orders_lambda.invoke_arn
}
fulfill_order = {
method_key = "fulfill_order"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.fulfillment_lambda.invoke_arn
}
}
stages = { prod = { stage_name = "prod" } }
}3 Β· Cognito User Pool authorizer
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "member-portal-api"
resources = {
profile = { path_part = "profile" }
}
authorizers = {
cognito = {
name = "cognito-authorizer"
type = "COGNITO_USER_POOLS"
provider_arns = [module.member_pool.arn] # tf-mod-aws-cognito
}
}
methods = {
get_profile = {
resource_key = "profile"
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_key = "cognito"
}
}
integrations = {
get_profile = {
method_key = "get_profile"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.profile_lambda.invoke_arn
}
}
stages = { prod = { stage_name = "prod" } }
}4 Β· API-key + usage-plan metered API
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "partner-data-api"
resources = { data = { path_part = "data" } }
methods = {
get_data = {
resource_key = "data"
http_method = "GET"
authorization = "NONE"
api_key_required = true
}
}
integrations = {
get_data = {
method_key = "get_data"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.data_lambda.invoke_arn
}
}
stages = { prod = { stage_name = "prod" } }
usage_plans = {
partner = {
name = "partner-plan"
api_stages = [
{ stage_key = "prod" }
]
throttle_settings = { burst_limit = 20, rate_limit = 10 }
quota_settings = { limit = 100000, period = "MONTH" }
}
}
api_keys = {
acme = { name = "acme-corp" }
}
usage_plan_keys = {
acme = { usage_plan_key = "partner", api_key_key = "acme" }
}
}5 Β· Private VPC-endpoint API
data "aws_iam_policy_document" "private_api" {
statement {
effect = "Allow"
principals { type = "AWS", identifiers = ["*"] }
actions = ["execute-api:Invoke"]
resources = ["execute-api:/*"]
condition {
test = "StringEquals"
variable = "aws:SourceVpce"
values = [module.execute_api_endpoint.id] # tf-mod-aws-vpc-endpoint
}
}
}
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "internal-only-api"
endpoint_configuration = {
types = ["PRIVATE"]
vpc_endpoint_ids = [module.execute_api_endpoint.id]
}
policy = data.aws_iam_policy_document.private_api.json
resources = { internal = { path_part = "internal" } }
methods = {
get_internal = {
resource_key = "internal"
http_method = "GET"
authorization = "AWS_IAM"
}
}
integrations = {
get_internal = {
method_key = "get_internal"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.internal_lambda.invoke_arn
}
}
stages = { prod = { stage_name = "prod" } }
}6 Β· Custom domain + base path mapping
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "public-api"
#... resources / methods / integrations / stages omitted for brevity...
stages = { prod = { stage_name = "prod" } }
domain_names = {
api = {
domain_name = "api.example.com"
regional_certificate_arn = module.api_cert.arn # tf-mod-aws-acm (regional, same Region)
security_policy = "TLS_1_2"
endpoint_configuration = { types = ["REGIONAL"] }
}
}
base_path_mappings = {
api = { domain_name_key = "api", stage_key = "prod" }
}
}
resource "aws_route53_record" "api" {
zone_id = module.public_zone.zone_id # tf-mod-aws-route53-zone
name = "api.example.com"
type = "A"
alias {
name = module.rest_api.domain_name_regional_domain_names["api"]
zone_id = module.rest_api.domain_name_regional_zone_ids["api"]
evaluate_target_health = true
}
}7 Β· VPC Link private integration to an internal NLB
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "internal-service-api"
vpc_links = {
internal_nlb = {
name = "internal-nlb-link"
target_arns = [module.internal_nlb.arn] # tf-mod-aws-lb, load_balancer_type = "network"
}
}
resources = { svc = { path_part = "svc" } }
methods = {
get_svc = {
resource_key = "svc"
http_method = "GET"
authorization = "AWS_IAM"
}
}
integrations = {
get_svc = {
method_key = "get_svc"
type = "HTTP_PROXY"
integration_http_method = "GET"
uri = "http://internal-service.example.internal/svc"
connection_type = "VPC_LINK"
vpc_link_key = "internal_nlb"
}
}
stages = { prod = { stage_name = "prod" } }
}8 Β· Request validation + models
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "validated-api"
resources = { widgets = { path_part = "widgets" } }
models = {
widget = {
name = "Widget"
schema = jsonencode({
"$schema" = "http://json-schema.org/draft-04/schema#"
title = "Widget"
type = "object"
required = ["name"]
properties = { name = { type = "string" } }
})
}
}
request_validators = {
body_only = { name = "validate-body", validate_request_body = true }
}
methods = {
create_widget = {
resource_key = "widgets"
http_method = "POST"
authorization = "AWS_IAM"
request_validator_key = "body_only"
request_models = { "application/json" = "Widget" }
}
}
integrations = {
create_widget = {
method_key = "create_widget"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.widgets_lambda.invoke_arn
}
}
stages = { prod = { stage_name = "prod" } }
}9 Β· Custom gateway responses (branded error bodies)
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "branded-errors-api"
#... resources / methods / integrations / stages omitted for brevity...
stages = { prod = { stage_name = "prod" } }
gateway_responses = {
unauthorized = {
response_type = "UNAUTHORIZED"
status_code = "401"
response_templates = {
"application/json" = jsonencode({ message = "$context.error.messageString" })
}
}
throttled = {
response_type = "THROTTLED"
status_code = "429"
response_templates = {
"application/json" = jsonencode({ message = "Too many requests, slow down." })
}
}
}
}10 Β· `tags` example (merge with provider `default_tags`)
provider "aws" {
default_tags {
tags = {
managed-by = "terraform"
cost-center = "shared-platform"
}
}
}
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "tagged-api"
#... resources / methods / integrations / stages omitted for brevity...
stages = { prod = { stage_name = "prod" } }
tags = {
environment = "prod"
owner = "payments-team"
# "cost-center" intentionally omitted here -- inherited from default_tags;
# setting it here too would make this module's value win in tags_all.
}
}11 Β· Secure-by-default opt-out (EDGE endpoint, relaxed method-settings tracing)
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "public-edge-api" # documented exception: public-facing marketing API, no PII
endpoint_configuration = {
types = ["EDGE"] # opt-out of the REGIONAL secure default
}
resources = { docs = { path_part = "docs" } }
methods = {
get_docs = {
resource_key = "docs"
http_method = "GET"
authorization = "NONE" # explicit, not a silent default
}
}
integrations = {
get_docs = {
method_key = "get_docs"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.docs_lambda.invoke_arn
}
}
stages = {
prod = {
stage_name = "prod"
xray_tracing_enabled = false # documented exception: no request cost budget for tracing
}
}
}12 Β· `for_each` pattern β one REST API per environment
locals {
environments = ["dev", "staging", "prod"]
}
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
for_each = toset(local.environments)
name = "orders-api-${each.key}"
resources = { orders = { path_part = "orders" } }
methods = {
list_orders = {
resource_key = "orders"
http_method = "GET"
authorization = "AWS_IAM"
}
}
integrations = {
list_orders = {
method_key = "list_orders"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.orders_lambda[each.key].invoke_arn
}
}
stages = { (each.key) = { stage_name = each.key } }
tags = { environment = each.key }
}13 Β· Documentation parts + version snapshot
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "documented-api"
#... resources / methods / integrations / stages omitted for brevity...
stages = { prod = { stage_name = "prod" } }
documentation_parts = {
api_overview = {
location = { type = "API" }
properties = jsonencode({ description = "Orders REST API -- internal use only." })
}
}
documentation_versions = {
v1 = { version = "1.0.0", description = "Initial GA release" }
}
}14 Β· Import an existing REST API
import {
to = module.rest_api.aws_api_gateway_rest_api.this
id = "abcde12345"
}
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "existing-api" # must match the imported name
#... reconstruct resources / methods / integrations / stages to match the
# live configuration before running terraform plan; import does not bring
# the body/OpenAPI content along automatically.
}15 Β· End-to-end composition (Lambda + Cognito + custom domain + usage plan)
module "orders_lambda" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-lambda?ref=v1.0.0"
function_name = "orders-api-handler"
#...
}
module "member_pool" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-cognito?ref=v1.0.0"
#...
}
module "api_cert" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-acm?ref=v1.0.0"
domain_name = "api.example.com"
# regional cert, same Region as the module below
}
module "apigw_logs" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-cloudwatch-log-group?ref=v1.0.0"
name = "/aws/apigateway/orders-api"
}
module "rest_api" {
source = "git::https://github.com/microsoftexpert/tf-mod-aws-api-gateway?ref=v1.0.0"
name = "orders-api"
description = "Orders REST API -- production"
resources = {
orders = { path_part = "orders" }
order_id = { parent_key = "orders", path_part = "{id}" }
}
authorizers = {
cognito = {
name = "member-authorizer"
type = "COGNITO_USER_POOLS"
provider_arns = [module.member_pool.arn]
}
}
methods = {
list_orders = {
resource_key = "orders"
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_key = "cognito"
}
get_order = {
resource_key = "order_id"
http_method = "GET"
authorization = "COGNITO_USER_POOLS"
authorizer_key = "cognito"
}
}
integrations = {
list_orders = {
method_key = "list_orders"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.orders_lambda.invoke_arn
}
get_order = {
method_key = "get_order"
type = "AWS_PROXY"
integration_http_method = "POST"
uri = module.orders_lambda.invoke_arn
}
}
stages = {
prod = {
stage_name = "prod"
access_log_destination_arn = module.apigw_logs.arn
access_log_format = jsonencode({ requestId = "$context.requestId", status = "$context.status" })
}
}
method_settings = {
all_prod = {
stage_key = "prod"
method_path = "*/*"
settings = { metrics_enabled = true, logging_level = "ERROR" }
}
}
domain_names = {
api = {
domain_name = "api.example.com"
regional_certificate_arn = module.api_cert.arn
security_policy = "TLS_1_2"
endpoint_configuration = { types = ["REGIONAL"] }
}
}
base_path_mappings = {
api = { domain_name_key = "api", stage_key = "prod" }
}
usage_plans = {
default = {
name = "orders-api-default"
api_stages = [{ stage_key = "prod" }]
throttle_settings = { burst_limit = 50, rate_limit = 25 }
}
}
tags = {
environment = "prod"
owner = "orders-team"
}
}
resource "aws_lambda_permission" "orders_api" {
statement_id = "AllowOrdersAPIInvoke"
action = "lambda:InvokeFunction"
function_name = module.orders_lambda.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${module.rest_api.execution_arn}/*/*"
}
resource "aws_route53_record" "api" {
zone_id = module.public_zone.zone_id
name = "api.example.com"
type = "A"
alias {
name = module.rest_api.domain_name_regional_domain_names["api"]
zone_id = module.rest_api.domain_name_regional_zone_ids["api"]
evaluate_target_health = true
}
}- Core:
name,description,endpoint_configuration,policy,api_key_source,binary_media_types,minimum_compression_size,disable_execute_api_endpoint,fail_on_warnings - Resource tree:
resources - Auth / schema:
authorizers,models,request_validators - Methods / integrations:
methods,method_responses,integrations,integration_responses,gateway_responses - Deployment / stages:
deployment_description,deployment_trigger_resources,deployment_variables,stages,method_settings - Client certificates:
client_certificates - Custom domains:
domain_names,domain_name_access_associations,base_path_mappings - Metering:
usage_plans,api_keys,usage_plan_keys - Networking:
vpc_links - Documentation:
documentation_parts,documentation_versions - Account:
manage_account_settings,account_cloudwatch_role_arn - Universal:
tags,timeouts
See variables.tf for the full deeply-typed schema of every map β each carries a heredoc description documenting every field.
- Primary:
id,arn,name,root_resource_id,execution_arn - Resource tree:
resource_ids,resource_paths - Methods / auth / schema:
method_ids,authorizer_ids,model_ids,request_validator_ids - Deployment / stages:
deployment_id,stage_ids,stage_arns,stage_invoke_urls,stage_execution_arns,stage_web_acl_arns - Custom domains:
domain_name_ids,domain_name_arns,domain_name_cloudfront_domain_names,domain_name_regional_domain_names,domain_name_regional_zone_ids,base_path_mapping_keys - Metering:
usage_plan_ids,usage_plan_arns,api_key_ids,api_key_arns,api_key_values(sensitive) - Networking:
vpc_link_ids - Client certificates:
client_certificate_ids,client_certificate_arns - Tags:
tags_all
api_key_values is sensitive = true β it never appears in plan/apply console output or logs.
- REGIONAL endpoints by default (
endpoint_configuration.types = ["REGIONAL"]) β opt into["EDGE"]or["PRIVATE"]deliberately. - No silent
"NONE"authorization β every method requires an explicitauthorizationvalue. - X-Ray tracing on by default (
stages[*].xray_tracing_enabled = true) β opt out per stage with a documented exception. - No accidental PII logging β
method_settings[*].settings.data_trace_enableddefaults tofalse; enable full request/response tracing only deliberately, per method. - Cache encryption on when caching is enabled (
cache_data_encrypted = true) and cache-control invalidation requires authorization (require_authorization_for_cache_control = true) by default. - Single authoring style β the OpenAPI/Swagger
body-import path (aws_api_gateway_rest_api_put) is deliberately out of scope; see SCOPE.md. - Account-level settings are opt-in and single-owner (
manage_account_settings = falseby default) to prevent two module calls fighting over the same Region-wide singleton. create_before_destroyalways on for the deployment, preventing "active stages" recreation failures.
cd tf-mod-aws-api-gateway
terraform init -backend=false
terraform validate
terraform fmt -check
β οΈ terraform plan/terraform applyrequire valid AWS credentials (profile / SSO / OIDC) and inherit the Region from the caller's provider block. Always pin the module source with?ref=v1.0.0, never a branch.
terraform validateandterraform fmt -checkare the offline gate for this module (no AWS credentials required).- For a live smoke test, apply the "Minimal Lambda-proxy REST API" example against a non-production account/Region, invoke
stage_invoke_urls["dev"], thenterraform destroy. - Run the shared
validate_all_modules.ps1harness (tflint + checkov) before committing.
$ terraform apply
+ resource "aws_api_gateway_rest_api" "this" {
+ id = (known after apply)
+ name = "orders-api"
...
}
Apply complete! Resources: 9 added, 0 changed, 0 destroyed.
Outputs:
id = "a1b2c3d4e5"
arn = "arn:aws:apigateway:us-east-2::/restapis/a1b2c3d4e5"
execution_arn = "arn:aws:execute-api:us-east-2:123456789012:a1b2c3d4e5"
stage_invoke_urls = {
"prod" = "https://a1b2c3d4e5.execute-api.us-east-2.amazonaws.com/prod"
}
BadRequestException: Active stages pointing to this deployment must be moved or deletedβ you disabledcreate_before_destroyon the deployment somewhere downstream, or manually edited the module. This module bakescreate_before_destroy = truein unconditionally; if you see this, check for aterraform state rm/manual edit that dropped the lifecycle block.- Redeployment doesn't pick up a change you expect β the automatic
deployment_hashonly coversresources/methods/integrations/integration_responses/method_responses/authorizers/models/request_validators/gateway_responses. If you changed something outside those maps (e.g. an external file consumed indirectly), fold a hash of it intodeployment_trigger_resources. - Tag drift from
default_tagsoverlap β don't set the same tag key in both the provider'sdefault_tagsand this module'stags; resource tags win, and duplicating the key just hides which value is actually in effect. Checktags_allfor the merged result. - Credential-chain failures on
plan/applyβ confirmAWS_PROFILE/ SSO session / OIDC role is active and the Region is set at the provider level; this module has no credential or region variables of its own. - EDGE custom domain "certificate must be in us-east-1" β
domain_names[*].certificate_arn(EDGE) must come from atf-mod-aws-acmcall usingproviders = { aws = aws.us_east_1 };regional_certificate_arn(REGIONAL/PRIVATE) must come from a call in this module's own Region. Mixing them up is the most common domain-name apply failure. - IAM permission denials on
applyβ API Gateway's IAM namespace is HTTP-verb-based (apigateway:POST/PATCH/etc.) scoped to resource-path ARNs, not per-resource-type actions; check the SCOPE.md## Required IAM permissionstable and widen the resource-path pattern, not the verb list. - PRIVATE API returns "Forbidden" for all callers β the resource policy (
var.policy) is missing or doesn't scopeexecute-api:Invoketo the VPC endpoint(s) inendpoint_configuration.vpc_endpoint_ids; both must be wired together. - Usage plan apply fails referencing a stage β the stage must exist before the usage plan's
api_stagesblock references it; this module orders it automatically via resource references, but if you see this externally, check for out-of-band stage deletion. aws_api_gateway_accountconflicts across module calls β only one module call per account/Region should setmanage_account_settings = true; a second call fighting over the same singleton causes perpetual plan diffs.- Destroy hangs or fails on dependency β usage-plan keys, base path mappings, and domain-name access associations must be destroyed before the domain name / usage plan / API key beneath them; Terraform sequences this automatically via this module's resource references, but manual
terraform state rmoperations can break the ordering.
- Terraform Registry:
hashicorp/awsprovider β API Gateway resource family (aws_api_gateway_rest_apiand siblings) - AWS API Gateway Developer Guide β REST API concepts, custom domain names, usage plans, private APIs
- AWS API Gateway Developer Guide β CloudWatch logging and X-Ray tracing setup
tf-mod-aws-api-gateway-v2β the HTTP/WebSocket (V2) sibling moduletf-mod-aws-lambda,tf-mod-aws-cognito,tf-mod-aws-acm,tf-mod-aws-lb,tf-mod-aws-vpc-endpoint,tf-mod-aws-wafv2,tf-mod-aws-route53-zoneβ upstream/downstream sibling modules
π§‘ "Infrastructure as Code should be standardized, consistent, and secure."