mirror of
https://github.com/ghndrx/terraform-foundation.git
synced 2026-02-10 06:45:06 +00:00
feat: Terraform Foundation - AWS Landing Zone
Enterprise-grade multi-tenant AWS cloud foundation. Modules: - GitHub OIDC for keyless CI/CD authentication - IAM account settings and security baseline - AWS Config Rules for compliance - ABAC (Attribute-Based Access Control) - SCPs (Service Control Policies) Features: - Multi-account architecture - Cost optimization patterns - Security best practices - Comprehensive documentation Tech: Terraform, AWS Organizations, IAM Identity Center
This commit is contained in:
532
terraform/05-workloads/_template/api-gateway/main.tf
Normal file
532
terraform/05-workloads/_template/api-gateway/main.tf
Normal file
@@ -0,0 +1,532 @@
|
||||
################################################################################
|
||||
# Workload: API Gateway REST API
|
||||
#
|
||||
# Deploys a REST API with:
|
||||
# - API Gateway with stages
|
||||
# - Lambda or HTTP backend integrations
|
||||
# - Custom domain with ACM
|
||||
# - WAF integration (optional)
|
||||
# - CloudWatch logging
|
||||
# - Usage plans and API keys
|
||||
#
|
||||
# Usage:
|
||||
# Copy this folder to 05-workloads/<tenant>-<api-name>/
|
||||
# Update locals
|
||||
# terraform init -backend-config=../../00-bootstrap/backend.hcl
|
||||
# terraform apply
|
||||
################################################################################
|
||||
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
aws = {
|
||||
source = "hashicorp/aws"
|
||||
version = ">= 5.0"
|
||||
}
|
||||
}
|
||||
|
||||
backend "s3" {
|
||||
key = "05-workloads/<TENANT>-<NAME>-api/terraform.tfstate"
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Configuration - UPDATE THESE
|
||||
################################################################################
|
||||
|
||||
locals {
|
||||
# Naming
|
||||
tenant = "<TENANT>"
|
||||
name = "<NAME>"
|
||||
env = "prod"
|
||||
|
||||
api_name = "${local.tenant}-${local.name}-${local.env}"
|
||||
|
||||
# API Type: REST or HTTP
|
||||
api_type = "REST" # REST for full features, HTTP for simpler/cheaper
|
||||
|
||||
# Custom domain (set to null to skip)
|
||||
domain_name = null # e.g., "api.example.com"
|
||||
hosted_zone_id = null # Route53 zone ID
|
||||
|
||||
# WAF (requires waf-alb module deployed)
|
||||
waf_acl_arn = null
|
||||
|
||||
# Stages
|
||||
stages = ["prod", "staging"]
|
||||
|
||||
# Throttling defaults
|
||||
throttle_burst_limit = 100
|
||||
throttle_rate_limit = 50
|
||||
|
||||
# CloudWatch logging
|
||||
logging_level = "INFO" # OFF, ERROR, INFO
|
||||
|
||||
# Lambda integrations (map of path -> lambda ARN)
|
||||
lambda_integrations = {
|
||||
# "GET /users" = "arn:aws:lambda:us-east-1:123456789012:function:get-users"
|
||||
# "POST /users" = "arn:aws:lambda:us-east-1:123456789012:function:create-user"
|
||||
# "GET /users/{id}" = "arn:aws:lambda:us-east-1:123456789012:function:get-user"
|
||||
}
|
||||
|
||||
# HTTP proxy integrations (map of path -> HTTP endpoint)
|
||||
http_integrations = {
|
||||
# "GET /health" = "https://backend.example.com/health"
|
||||
}
|
||||
|
||||
# Mock integrations for static responses
|
||||
mock_integrations = {
|
||||
"GET /health" = {
|
||||
status_code = "200"
|
||||
response = jsonencode({ status = "healthy" })
|
||||
}
|
||||
}
|
||||
|
||||
# CORS configuration
|
||||
cors_enabled = true
|
||||
cors_origins = ["*"]
|
||||
cors_methods = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
cors_headers = ["Content-Type", "Authorization", "X-Api-Key"]
|
||||
|
||||
# API Keys and Usage Plans
|
||||
create_api_key = true
|
||||
usage_plans = {
|
||||
basic = {
|
||||
quota_limit = 1000
|
||||
quota_period = "MONTH"
|
||||
throttle_burst = 10
|
||||
throttle_rate = 5
|
||||
}
|
||||
premium = {
|
||||
quota_limit = 100000
|
||||
quota_period = "MONTH"
|
||||
throttle_burst = 100
|
||||
throttle_rate = 50
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Variables
|
||||
################################################################################
|
||||
|
||||
variable "region" {
|
||||
type = string
|
||||
default = "us-east-1"
|
||||
}
|
||||
|
||||
variable "state_bucket" {
|
||||
type = string
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Provider
|
||||
################################################################################
|
||||
|
||||
provider "aws" {
|
||||
region = var.region
|
||||
|
||||
default_tags {
|
||||
tags = {
|
||||
Tenant = local.tenant
|
||||
App = local.name
|
||||
Environment = local.env
|
||||
ManagedBy = "terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Data Sources
|
||||
################################################################################
|
||||
|
||||
data "aws_caller_identity" "current" {}
|
||||
data "aws_region" "current" {}
|
||||
|
||||
################################################################################
|
||||
# REST API
|
||||
################################################################################
|
||||
|
||||
resource "aws_api_gateway_rest_api" "main" {
|
||||
name = local.api_name
|
||||
description = "REST API for ${local.tenant} ${local.name}"
|
||||
|
||||
endpoint_configuration {
|
||||
types = ["REGIONAL"]
|
||||
}
|
||||
|
||||
tags = { Name = local.api_name }
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# CloudWatch Logging
|
||||
################################################################################
|
||||
|
||||
resource "aws_cloudwatch_log_group" "api" {
|
||||
name = "/aws/api-gateway/${local.api_name}"
|
||||
retention_in_days = 30
|
||||
|
||||
tags = { Name = local.api_name }
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_account" "main" {
|
||||
cloudwatch_role_arn = aws_iam_role.api_logging.arn
|
||||
}
|
||||
|
||||
resource "aws_iam_role" "api_logging" {
|
||||
name = "${local.api_name}-api-logging"
|
||||
|
||||
assume_role_policy = jsonencode({
|
||||
Version = "2012-10-17"
|
||||
Statement = [{
|
||||
Effect = "Allow"
|
||||
Action = "sts:AssumeRole"
|
||||
Principal = { Service = "apigateway.amazonaws.com" }
|
||||
}]
|
||||
})
|
||||
|
||||
tags = { Name = "${local.api_name}-api-logging" }
|
||||
}
|
||||
|
||||
resource "aws_iam_role_policy_attachment" "api_logging" {
|
||||
role = aws_iam_role.api_logging.name
|
||||
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Mock Integration - Health Check
|
||||
################################################################################
|
||||
|
||||
resource "aws_api_gateway_resource" "health" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
parent_id = aws_api_gateway_rest_api.main.root_resource_id
|
||||
path_part = "health"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method" "health_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = "GET"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "health_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_get.http_method
|
||||
type = "MOCK"
|
||||
|
||||
request_templates = {
|
||||
"application/json" = jsonencode({ statusCode = 200 })
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method_response" "health_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_get.http_method
|
||||
status_code = "200"
|
||||
|
||||
response_models = {
|
||||
"application/json" = "Empty"
|
||||
}
|
||||
|
||||
response_parameters = {
|
||||
"method.response.header.Access-Control-Allow-Origin" = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration_response" "health_get" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_get.http_method
|
||||
status_code = aws_api_gateway_method_response.health_get.status_code
|
||||
|
||||
response_templates = {
|
||||
"application/json" = jsonencode({
|
||||
status = "healthy"
|
||||
timestamp = "$context.requestTime"
|
||||
})
|
||||
}
|
||||
|
||||
response_parameters = {
|
||||
"method.response.header.Access-Control-Allow-Origin" = "'*'"
|
||||
}
|
||||
|
||||
depends_on = [aws_api_gateway_integration.health_get]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# CORS - OPTIONS method for health
|
||||
################################################################################
|
||||
|
||||
resource "aws_api_gateway_method" "health_options" {
|
||||
count = local.cors_enabled ? 1 : 0
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = "OPTIONS"
|
||||
authorization = "NONE"
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration" "health_options" {
|
||||
count = local.cors_enabled ? 1 : 0
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_options[0].http_method
|
||||
type = "MOCK"
|
||||
|
||||
request_templates = {
|
||||
"application/json" = jsonencode({ statusCode = 200 })
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method_response" "health_options" {
|
||||
count = local.cors_enabled ? 1 : 0
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_options[0].http_method
|
||||
status_code = "200"
|
||||
|
||||
response_parameters = {
|
||||
"method.response.header.Access-Control-Allow-Headers" = true
|
||||
"method.response.header.Access-Control-Allow-Methods" = true
|
||||
"method.response.header.Access-Control-Allow-Origin" = true
|
||||
}
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_integration_response" "health_options" {
|
||||
count = local.cors_enabled ? 1 : 0
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
resource_id = aws_api_gateway_resource.health.id
|
||||
http_method = aws_api_gateway_method.health_options[0].http_method
|
||||
status_code = "200"
|
||||
|
||||
response_parameters = {
|
||||
"method.response.header.Access-Control-Allow-Headers" = "'${join(",", local.cors_headers)}'"
|
||||
"method.response.header.Access-Control-Allow-Methods" = "'${join(",", local.cors_methods)}'"
|
||||
"method.response.header.Access-Control-Allow-Origin" = "'${join(",", local.cors_origins)}'"
|
||||
}
|
||||
|
||||
depends_on = [aws_api_gateway_integration.health_options]
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Deployment & Stages
|
||||
################################################################################
|
||||
|
||||
resource "aws_api_gateway_deployment" "main" {
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
|
||||
triggers = {
|
||||
redeployment = sha1(jsonencode([
|
||||
aws_api_gateway_resource.health.id,
|
||||
aws_api_gateway_method.health_get.id,
|
||||
aws_api_gateway_integration.health_get.id,
|
||||
]))
|
||||
}
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
|
||||
depends_on = [
|
||||
aws_api_gateway_integration.health_get,
|
||||
aws_api_gateway_integration_response.health_get,
|
||||
]
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_stage" "stages" {
|
||||
for_each = toset(local.stages)
|
||||
|
||||
deployment_id = aws_api_gateway_deployment.main.id
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
stage_name = each.value
|
||||
|
||||
access_log_settings {
|
||||
destination_arn = aws_cloudwatch_log_group.api.arn
|
||||
format = jsonencode({
|
||||
requestId = "$context.requestId"
|
||||
ip = "$context.identity.sourceIp"
|
||||
caller = "$context.identity.caller"
|
||||
user = "$context.identity.user"
|
||||
requestTime = "$context.requestTime"
|
||||
httpMethod = "$context.httpMethod"
|
||||
resourcePath = "$context.resourcePath"
|
||||
status = "$context.status"
|
||||
protocol = "$context.protocol"
|
||||
responseLength = "$context.responseLength"
|
||||
integrationLatency = "$context.integrationLatency"
|
||||
})
|
||||
}
|
||||
|
||||
tags = { Name = "${local.api_name}-${each.value}" }
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_method_settings" "stages" {
|
||||
for_each = toset(local.stages)
|
||||
|
||||
rest_api_id = aws_api_gateway_rest_api.main.id
|
||||
stage_name = aws_api_gateway_stage.stages[each.value].stage_name
|
||||
method_path = "*/*"
|
||||
|
||||
settings {
|
||||
logging_level = local.logging_level
|
||||
data_trace_enabled = local.logging_level != "OFF"
|
||||
metrics_enabled = true
|
||||
|
||||
throttling_burst_limit = local.throttle_burst_limit
|
||||
throttling_rate_limit = local.throttle_rate_limit
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# WAF Association (Optional)
|
||||
################################################################################
|
||||
|
||||
resource "aws_wafv2_web_acl_association" "api" {
|
||||
count = local.waf_acl_arn != null ? length(local.stages) : 0
|
||||
resource_arn = aws_api_gateway_stage.stages[local.stages[count.index]].arn
|
||||
web_acl_arn = local.waf_acl_arn
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Custom Domain (Optional)
|
||||
################################################################################
|
||||
|
||||
resource "aws_acm_certificate" "api" {
|
||||
count = local.domain_name != null ? 1 : 0
|
||||
domain_name = local.domain_name
|
||||
validation_method = "DNS"
|
||||
|
||||
lifecycle {
|
||||
create_before_destroy = true
|
||||
}
|
||||
|
||||
tags = { Name = local.domain_name }
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "cert_validation" {
|
||||
for_each = local.domain_name != null ? {
|
||||
for dvo in aws_acm_certificate.api[0].domain_validation_options : dvo.domain_name => {
|
||||
name = dvo.resource_record_name
|
||||
record = dvo.resource_record_value
|
||||
type = dvo.resource_record_type
|
||||
}
|
||||
} : {}
|
||||
|
||||
zone_id = local.hosted_zone_id
|
||||
name = each.value.name
|
||||
type = each.value.type
|
||||
records = [each.value.record]
|
||||
ttl = 60
|
||||
}
|
||||
|
||||
resource "aws_acm_certificate_validation" "api" {
|
||||
count = local.domain_name != null ? 1 : 0
|
||||
certificate_arn = aws_acm_certificate.api[0].arn
|
||||
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_domain_name" "main" {
|
||||
count = local.domain_name != null ? 1 : 0
|
||||
domain_name = local.domain_name
|
||||
certificate_arn = aws_acm_certificate_validation.api[0].certificate_arn
|
||||
|
||||
endpoint_configuration {
|
||||
types = ["REGIONAL"]
|
||||
}
|
||||
|
||||
tags = { Name = local.domain_name }
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_base_path_mapping" "main" {
|
||||
count = local.domain_name != null ? 1 : 0
|
||||
api_id = aws_api_gateway_rest_api.main.id
|
||||
stage_name = aws_api_gateway_stage.stages["prod"].stage_name
|
||||
domain_name = aws_api_gateway_domain_name.main[0].domain_name
|
||||
}
|
||||
|
||||
resource "aws_route53_record" "api" {
|
||||
count = local.domain_name != null ? 1 : 0
|
||||
zone_id = local.hosted_zone_id
|
||||
name = local.domain_name
|
||||
type = "A"
|
||||
|
||||
alias {
|
||||
name = aws_api_gateway_domain_name.main[0].regional_domain_name
|
||||
zone_id = aws_api_gateway_domain_name.main[0].regional_zone_id
|
||||
evaluate_target_health = false
|
||||
}
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# API Keys & Usage Plans
|
||||
################################################################################
|
||||
|
||||
resource "aws_api_gateway_api_key" "main" {
|
||||
count = local.create_api_key ? 1 : 0
|
||||
name = "${local.api_name}-key"
|
||||
enabled = true
|
||||
|
||||
tags = { Name = "${local.api_name}-key" }
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_usage_plan" "plans" {
|
||||
for_each = local.usage_plans
|
||||
|
||||
name = "${local.api_name}-${each.key}"
|
||||
|
||||
api_stages {
|
||||
api_id = aws_api_gateway_rest_api.main.id
|
||||
stage = aws_api_gateway_stage.stages["prod"].stage_name
|
||||
}
|
||||
|
||||
quota_settings {
|
||||
limit = each.value.quota_limit
|
||||
period = each.value.quota_period
|
||||
}
|
||||
|
||||
throttle_settings {
|
||||
burst_limit = each.value.throttle_burst
|
||||
rate_limit = each.value.throttle_rate
|
||||
}
|
||||
|
||||
tags = { Name = "${local.api_name}-${each.key}" }
|
||||
}
|
||||
|
||||
resource "aws_api_gateway_usage_plan_key" "main" {
|
||||
count = local.create_api_key ? 1 : 0
|
||||
key_id = aws_api_gateway_api_key.main[0].id
|
||||
key_type = "API_KEY"
|
||||
usage_plan_id = aws_api_gateway_usage_plan.plans["basic"].id
|
||||
}
|
||||
|
||||
################################################################################
|
||||
# Outputs
|
||||
################################################################################
|
||||
|
||||
output "api_id" {
|
||||
value = aws_api_gateway_rest_api.main.id
|
||||
}
|
||||
|
||||
output "api_name" {
|
||||
value = aws_api_gateway_rest_api.main.name
|
||||
}
|
||||
|
||||
output "stage_urls" {
|
||||
value = { for stage in local.stages : stage => aws_api_gateway_stage.stages[stage].invoke_url }
|
||||
}
|
||||
|
||||
output "custom_domain_url" {
|
||||
value = local.domain_name != null ? "https://${local.domain_name}" : null
|
||||
}
|
||||
|
||||
output "api_key" {
|
||||
value = local.create_api_key ? aws_api_gateway_api_key.main[0].value : null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "health_endpoint" {
|
||||
value = "${aws_api_gateway_stage.stages["prod"].invoke_url}health"
|
||||
}
|
||||
Reference in New Issue
Block a user