fix(infra): own a dedicated VPC for Fargate (PLAT-216)

Prod has no default VPC; 10.70 is unused and matches the meals seam.
This commit is contained in:
Adam Moussa 2026-09-21 15:23:41 -04:00
parent 26adb8e6c0
commit d18cab7b8f
No known key found for this signature in database
5 changed files with 137 additions and 33 deletions

View file

@ -2,22 +2,6 @@
# image; Terraform ignores container_definitions after the bootstrap task
# definition. Dual-run with API Gateway until the Fargate cutover.
data "aws_vpc" "default" {
default = true
}
data "aws_subnets" "default" {
filter {
name = "vpc-id"
values = [data.aws_vpc.default.id]
}
filter {
name = "default-for-az"
values = ["true"]
}
}
resource "aws_ecr_repository" "api" {
name = local.project
image_tag_mutability = "MUTABLE"
@ -56,7 +40,7 @@ resource "aws_ecr_lifecycle_policy" "api" {
resource "aws_security_group" "alb" {
name = "${local.project}-alb"
description = "Public ALB for afterhours-shift-manager"
vpc_id = data.aws_vpc.default.id
vpc_id = aws_vpc.this.id
ingress {
description = "HTTP from the internet (health and pre-DNS)"
@ -88,7 +72,7 @@ resource "aws_security_group" "alb" {
resource "aws_security_group" "api" {
name = "${local.project}-api"
description = "Fargate tasks for afterhours-shift-manager"
vpc_id = data.aws_vpc.default.id
vpc_id = aws_vpc.this.id
ingress {
description = "From ALB"
@ -111,7 +95,7 @@ resource "aws_lb" "api" {
load_balancer_type = "application"
idle_timeout = 120
security_groups = [aws_security_group.alb.id]
subnets = data.aws_subnets.default.ids
subnets = aws_subnet.public[*].id
drop_invalid_header_fields = true
}
@ -120,7 +104,7 @@ resource "aws_lb_target_group" "api" {
name = "${local.project}-api"
port = 8080
protocol = "HTTP"
vpc_id = data.aws_vpc.default.id
vpc_id = aws_vpc.this.id
target_type = "ip"
health_check {
@ -276,7 +260,7 @@ resource "aws_ecs_service" "api" {
deployment_maximum_percent = 200
network_configuration {
subnets = data.aws_subnets.default.ids
subnets = aws_subnet.public[*].id
security_groups = [aws_security_group.api.id]
assign_public_ip = true
}

View file

@ -668,19 +668,41 @@ data "aws_iam_policy_document" "hcptf_apply_services" {
sid = "VpcSecurityGroups"
effect = "Allow"
actions = [
"ec2:DescribeVpcs",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeAccountAttributes",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:AssociateRouteTable",
"ec2:AttachInternetGateway",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
"ec2:RevokeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateInternetGateway",
"ec2:CreateRoute",
"ec2:CreateRouteTable",
"ec2:CreateSecurityGroup",
"ec2:CreateSubnet",
"ec2:CreateTags",
"ec2:CreateVpc",
"ec2:DeleteInternetGateway",
"ec2:DeleteRoute",
"ec2:DeleteRouteTable",
"ec2:DeleteSecurityGroup",
"ec2:DeleteSubnet",
"ec2:DeleteTags",
"ec2:DeleteVpc",
"ec2:DescribeAccountAttributes",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeInternetGateways",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeRouteTables",
"ec2:DescribeSecurityGroupRules",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeTags",
"ec2:DescribeVpcAttribute",
"ec2:DescribeVpcs",
"ec2:DetachInternetGateway",
"ec2:DisassociateRouteTable",
"ec2:ModifySubnetAttribute",
"ec2:ModifyVpcAttribute",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress",
]
resources = ["*"]
}
@ -907,6 +929,25 @@ data "aws_iam_policy_document" "hcptf_plan_refresh" {
resources = ["*"]
}
statement {
sid = "RefreshVpc"
effect = "Allow"
actions = [
"ec2:DescribeAccountAttributes",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeInternetGateways",
"ec2:DescribeNetworkInterfaces",
"ec2:DescribeRouteTables",
"ec2:DescribeSecurityGroupRules",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeTags",
"ec2:DescribeVpcAttribute",
"ec2:DescribeVpcs",
]
resources = ["*"]
}
statement {
sid = "RefreshEcr"
effect = "Allow"

View file

@ -14,8 +14,14 @@ locals {
artifacts_bucket_name = "afterhours-shift-manager-artifacts-${local.account_id}"
ssm_prefix = "/afterhours-shift-manager"
table_name = "afterhours-shifts"
site_alerts_arn = "arn:aws:sns:${var.aws_region}:${local.account_id}:site-alerts"
# Prod has no default VPC. 10.70 is unused in 011934824531
# (10.0 proposal-system, 10.20 payments-dashboard, 10.40 syslog,
# 10.60 meal-order-manager, 10.80 apm-wo).
vpc_cidr = "10.70.0.0/16"
public_subnet_cidrs = ["10.70.0.0/24", "10.70.1.0/24"]
table_name = "afterhours-shifts"
site_alerts_arn = "arn:aws:sns:${var.aws_region}:${local.account_id}:site-alerts"
github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com"
# Org has Actions OIDC use_immutable_subject=true.

58
terraform/vpc.tf Normal file
View file

@ -0,0 +1,58 @@
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "this" {
cidr_block = local.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${local.project}-vpc"
}
# First apply updates the live hcptf apply role before CreateVpc.
depends_on = [aws_iam_role_policy.hcptf_apply_services]
}
resource "aws_internet_gateway" "this" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${local.project}-igw"
}
}
resource "aws_subnet" "public" {
count = length(local.public_subnet_cidrs)
vpc_id = aws_vpc.this.id
cidr_block = local.public_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${local.project}-public-${count.index}"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
tags = {
Name = "${local.project}-public"
}
}
resource "aws_route" "public_default" {
route_table_id = aws_route_table.public.id
destination_cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
resource "aws_route_table_association" "public" {
count = length(local.public_subnet_cidrs)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}

View file

@ -68,6 +68,21 @@ def test_ecs_ignore_changes_and_task_size():
assert 'path = "/api/health"' in ecs
def test_stack_owns_a_vpc_instead_of_looking_up_default():
vpc = (TERRAFORM / "vpc.tf").read_text()
ecs = (TERRAFORM / "ecs.tf").read_text()
assert 'resource "aws_vpc" "this"' in vpc
assert "cidr_block = local.vpc_cidr" in vpc
assert 'vpc_cidr = "10.70.0.0/16"' in LOCALS
assert 'data "aws_vpc" "default"' not in ecs
assert "data.aws_vpc.default" not in ecs
assert "data.aws_subnets.default" not in ecs
assert "aws_vpc.this.id" in ecs
assert "aws_subnet.public[*].id" in ecs
assert "ec2:CreateVpc" in HCP_IAM
assert "sid = \"RefreshVpc\"" in HCP_IAM
def test_deploy_api_workflow_exists():
deploy_api = (ROOT / ".github" / "workflows" / "deploy-api.yaml").read_text()
assert "environment: ${{ needs.target.outputs.environment }}" in deploy_api