mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 14:43:12 +00:00
* feat(api): serve meals on ECS Fargate instead of Lambda Keep the Flask app always-on with in-process jobs so CloudFront no longer fronts a cold-start API Gateway. * fix(jobs): run delayed close and reminder deliveries Wall-clock skip windows dropped the only weekly SQS attempt when Scheduler already fired in Eastern time. Dev schedules stay disabled. * fix(api): return JSON objects and stop logging job payloads Flask now jsonify-s handler dicts so API responses are not HTML, and the worker logs only event and status. * fix(ci): restore the reusable workflow so the required check is named ci / ci Inlining the job reported `ci` instead of the org ruleset's `ci / ci`. * fix(secrets): drop unused os import so ruff check passes * style: apply ruff format so ci-python-app lint passes * fix(infra): give meals its own VPC because prod has none * chore(security): re-key ALB SG checkov suppression after vpc.tf
216 lines
7.1 KiB
HCL
216 lines
7.1 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 = 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
|
|
}
|
|
}
|