From d18cab7b8fd4569af31c350503553dda665d0ac2 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 21 Sep 2026 15:23:41 -0400 Subject: [PATCH] fix(infra): own a dedicated VPC for Fargate (PLAT-216) Prod has no default VPC; 10.70 is unused and matches the meals seam. --- terraform/ecs.tf | 26 +++----------- terraform/hcp_iam.tf | 61 ++++++++++++++++++++++++++------ terraform/locals.tf | 10 ++++-- terraform/vpc.tf | 58 ++++++++++++++++++++++++++++++ tests/infra/test_hcp_contract.py | 15 ++++++++ 5 files changed, 137 insertions(+), 33 deletions(-) create mode 100644 terraform/vpc.tf diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 722f522..89ac0e1 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -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 } diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index 3347cc3..8425e75 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -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" diff --git a/terraform/locals.tf b/terraform/locals.tf index 3884e9d..19facf0 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -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. diff --git a/terraform/vpc.tf b/terraform/vpc.tf new file mode 100644 index 0000000..1a8e625 --- /dev/null +++ b/terraform/vpc.tf @@ -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 +} diff --git a/tests/infra/test_hcp_contract.py b/tests/infra/test_hcp_contract.py index a2bc1fb..ad8f581 100644 --- a/tests/infra/test_hcp_contract.py +++ b/tests/infra/test_hcp_contract.py @@ -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