meal-order-manager/terraform/cloudfront.tf
Adam Moussa e8e0034221
feat(edge): proxy portal meals API paths on orders.seahaven.com (DEV-282)
Keep the static form on / while CloudFront forwards submit, form-status, admin, and caller-only order lookup so the portal can use the public meals host without a form redirect.
2026-09-17 19:34:03 -04:00

202 lines
6.5 KiB
HCL

resource "aws_cloudfront_origin_access_control" "form" {
name = "${local.project}-oac"
description = "OAC for the meal-order-manager form origin bucket"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_cache_policy" "menu_api" {
name = "${local.project}-menu-api"
comment = "Cache public menu responses for 60 seconds per request origin"
default_ttl = 60
max_ttl = 60
min_ttl = 60
parameters_in_cache_key_and_forwarded_to_origin {
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = ["Origin"]
}
}
query_strings_config {
query_string_behavior = "none"
}
enable_accept_encoding_brotli = true
enable_accept_encoding_gzip = true
}
}
resource "aws_cloudfront_origin_request_policy" "menu_api" {
name = "${local.project}-menu-api"
comment = "Forward CORS preflight headers to the HTTP API"
cookies_config {
cookie_behavior = "none"
}
headers_config {
header_behavior = "whitelist"
headers {
items = [
"Access-Control-Request-Headers",
"Access-Control-Request-Method",
]
}
}
query_strings_config {
query_string_behavior = "none"
}
}
# Rewrite extensionless form paths to index.html on the S3 origin only.
# A distribution-wide 403 custom error page would also rewrite API 403s
# (non-admin, bad bearer) into form HTML.
resource "aws_cloudfront_function" "form_spa_rewrite" {
name = "${local.project}-form-spa-rewrite"
runtime = "cloudfront-js-2.0"
comment = "Rewrite extensionless form paths to /index.html"
publish = true
code = <<-EOF
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.lastIndexOf('.') <= uri.lastIndexOf('/')) {
request.uri = '/index.html';
}
return request;
}
EOF
}
resource "aws_cloudfront_distribution" "form" {
enabled = true
is_ipv6_enabled = true
http_version = "http2and3"
comment = "meal-order-manager form hosting"
default_root_object = "index.html"
price_class = "PriceClass_100"
# Empty until DNS cutover: AWS rejects a second distribution claiming an
# alias whose DNS still points at another CloudFront distribution (mgmt).
aliases = var.attach_custom_domain ? [var.domain_name] : []
# Shared org CloudFront WAF (audit M-17), resolved from Parameter Store.
web_acl_id = data.aws_ssm_parameter.app_web_acl_arn.value
origin {
origin_id = "S3FormOrigin"
domain_name = aws_s3_bucket.form.bucket_regional_domain_name
origin_access_control_id = aws_cloudfront_origin_access_control.form.id
}
origin {
origin_id = "OrderApiOrigin"
domain_name = trimprefix(aws_apigatewayv2_api.order_api.api_endpoint, "https://")
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
ordered_cache_behavior {
path_pattern = "/api/menu/*"
target_origin_id = "OrderApiOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = aws_cloudfront_cache_policy.menu_api.id
origin_request_policy_id = aws_cloudfront_origin_request_policy.menu_api.id
}
# Do not use path `/api/*`. That would front IAM-only publish routes without SigV4.
ordered_cache_behavior {
path_pattern = "/api/form-status/*"
target_origin_id = "OrderApiOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = local.api_cache_policy_id
origin_request_policy_id = local.api_origin_request_policy_id
}
ordered_cache_behavior {
path_pattern = "/api/orders/*"
target_origin_id = "OrderApiOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = local.api_cache_policy_id
origin_request_policy_id = local.api_origin_request_policy_id
}
ordered_cache_behavior {
path_pattern = "/api/submit-order"
target_origin_id = "OrderApiOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = local.cloudfront_all_methods
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = local.api_cache_policy_id
origin_request_policy_id = local.api_origin_request_policy_id
}
ordered_cache_behavior {
path_pattern = "/api/admin/*"
target_origin_id = "OrderApiOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = local.cloudfront_all_methods
cached_methods = ["GET", "HEAD"]
compress = true
cache_policy_id = local.api_cache_policy_id
origin_request_policy_id = local.api_origin_request_policy_id
}
default_cache_behavior {
target_origin_id = "S3FormOrigin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
compress = true
# AWS managed policy: CachingDisabled. The form HTML is republished weekly
# and read through a signed API, so a stale edge copy is worse than an
# origin fetch.
cache_policy_id = local.api_cache_policy_id
function_association {
event_type = "viewer-request"
function_arn = aws_cloudfront_function.form_spa_rewrite.arn
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = !var.attach_custom_domain
acm_certificate_arn = var.attach_custom_domain ? data.aws_acm_certificate.orders.arn : null
ssl_support_method = var.attach_custom_domain ? "sni-only" : null
minimum_protocol_version = var.attach_custom_domain ? "TLSv1.2_2021" : null
}
lifecycle {
prevent_destroy = true
}
}