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 = aws_lb.api.dns_name custom_origin_config { http_port = 80 https_port = 443 origin_protocol_policy = "http-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 HMAC publish routes. ordered_cache_behavior { path_pattern = "/api/roster" 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/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[0].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 cannot interpolate. Keep it on so a tagged apply cannot # destroy the live distribution; tear down a leftover dev distribution # from the AWS console. prevent_destroy = true } }