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" } } 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 } 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 = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" } # A request for an object the private origin does not hold returns 403, not # 404. Rewriting it to the form keeps deep links working, matching the SAM # template. custom_error_response { error_code = 403 response_code = 200 response_page_path = "/index.html" } 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 } }