meal-order-manager/terraform/s3.tf
Adam Moussa f48a82c476
Some checks are pending
Deploy API / Resolve target (push) Waiting to run
Deploy API / Deploy API to (push) Blocked by required conditions
feat(api): serve meals on ECS Fargate instead of Lambda (PLAT-215) (#199)
* 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
2026-09-21 19:34:24 +00:00

209 lines
4.3 KiB
HCL

# Form-hosting and reports buckets.
# ---------------------------------------------------------------------------
# Form bucket — private origin for the CloudFront distribution
# ---------------------------------------------------------------------------
resource "aws_s3_bucket" "form" {
bucket = local.form_bucket_name
tags = {
Purpose = "meal-order-form-hosting"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "form" {
bucket = aws_s3_bucket.form.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_ownership_controls" "form" {
bucket = aws_s3_bucket.form.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "form" {
bucket = aws_s3_bucket.form.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "form" {
bucket = aws_s3_bucket.form.id
rule {
id = "delete-old-archives"
status = "Enabled"
filter {
prefix = "archive/"
}
expiration {
days = 90
}
}
}
data "aws_iam_policy_document" "form" {
statement {
sid = "DenyInsecureTransport"
effect = "Deny"
principals {
type = "*"
identifiers = ["*"]
}
actions = ["s3:*"]
resources = [
aws_s3_bucket.form.arn,
"${aws_s3_bucket.form.arn}/*",
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
statement {
sid = "AllowCloudFrontOAC"
effect = "Allow"
principals {
type = "Service"
identifiers = ["cloudfront.amazonaws.com"]
}
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.form.arn}/*"]
condition {
test = "StringEquals"
variable = "AWS:SourceArn"
values = [aws_cloudfront_distribution.form.arn]
}
}
}
resource "aws_s3_bucket_policy" "form" {
bucket = aws_s3_bucket.form.id
policy = data.aws_iam_policy_document.form.json
depends_on = [aws_s3_bucket_public_access_block.form]
}
# ---------------------------------------------------------------------------
# Reports bucket — payroll CSVs and weekly summary PDFs
# ---------------------------------------------------------------------------
resource "aws_s3_bucket" "reports" {
bucket = local.reports_bucket_name
tags = {
Purpose = "meal-order-reports"
}
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_public_access_block" "reports" {
bucket = aws_s3_bucket.reports.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_ownership_controls" "reports" {
bucket = aws_s3_bucket.reports.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "reports" {
bucket = aws_s3_bucket.reports.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_lifecycle_configuration" "reports" {
bucket = aws_s3_bucket.reports.id
# Whole-bucket rule, matching the SAM template's unprefixed rule.
rule {
id = "archive-old-reports"
status = "Enabled"
filter {
prefix = ""
}
transition {
days = 90
storage_class = "GLACIER_IR"
}
}
}
# The SAM template attached no policy to this bucket. The TLS-only deny is a
# deliberate addition; it grants nothing and blocks plaintext access to payroll
# data.
data "aws_iam_policy_document" "reports" {
statement {
sid = "DenyInsecureTransport"
effect = "Deny"
principals {
type = "*"
identifiers = ["*"]
}
actions = ["s3:*"]
resources = [
aws_s3_bucket.reports.arn,
"${aws_s3_bucket.reports.arn}/*",
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
}
resource "aws_s3_bucket_policy" "reports" {
bucket = aws_s3_bucket.reports.id
policy = data.aws_iam_policy_document.reports.json
depends_on = [aws_s3_bucket_public_access_block.reports]
}