From 6573312bb34ac359db9487ddf0afa99d90202d2a Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 21 Sep 2026 15:20:36 -0400 Subject: [PATCH] fix(infra): give meals its own VPC because prod has none --- terraform/ecs.tf | 26 ++++------------- terraform/hcp_iam.tf | 56 ++++++++++++++++++++++++++--------- terraform/locals.tf | 5 ++++ terraform/vpc.tf | 58 +++++++++++++++++++++++++++++++++++++ tests/test_terraform_vpc.py | 26 +++++++++++++++++ 5 files changed, 137 insertions(+), 34 deletions(-) create mode 100644 terraform/vpc.tf create mode 100644 tests/test_terraform_vpc.py diff --git a/terraform/ecs.tf b/terraform/ecs.tf index 71d721e..d15fecc 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -1,22 +1,6 @@ # Always-on meals API: Fargate behind an ALB. GitHub Actions owns the image; # Terraform ignores container_definitions after the bootstrap task definition. -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" @@ -55,7 +39,7 @@ resource "aws_ecr_lifecycle_policy" "api" { resource "aws_security_group" "alb" { name = "${local.project}-alb" description = "Public ALB for meal-order-manager" - vpc_id = data.aws_vpc.default.id + vpc_id = aws_vpc.this.id ingress { # CloudFront prefix lists cannot cover GitHub-hosted weekly-menu HMAC @@ -79,7 +63,7 @@ resource "aws_security_group" "alb" { resource "aws_security_group" "api" { name = "${local.project}-api" description = "Fargate tasks for meal-order-manager" - vpc_id = data.aws_vpc.default.id + vpc_id = aws_vpc.this.id ingress { description = "From ALB" @@ -102,7 +86,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 } @@ -111,7 +95,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 { @@ -218,7 +202,7 @@ resource "aws_ecs_service" "api" { launch_type = "FARGATE" 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 250fd96..96ffb17 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -464,23 +464,45 @@ resource "aws_iam_role_policy" "hcptf_apply_services" { }, { Action = [ - "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", ] Resource = "*" Effect = "Allow" - Sid = "VpcSecurityGroups" + Sid = "Ec2VpcManagement" }, { Action = [ @@ -858,9 +880,17 @@ resource "aws_iam_role_policy" "hcptf_plan_refresh" { }, { Action = [ - "ec2:DescribeVpcs", - "ec2:DescribeSubnets", + "ec2:DescribeAccountAttributes", + "ec2:DescribeAvailabilityZones", + "ec2:DescribeInternetGateways", + "ec2:DescribeNetworkInterfaces", + "ec2:DescribeRouteTables", + "ec2:DescribeSecurityGroupRules", "ec2:DescribeSecurityGroups", + "ec2:DescribeSubnets", + "ec2:DescribeTags", + "ec2:DescribeVpcAttribute", + "ec2:DescribeVpcs", ] Resource = "*" Effect = "Allow" diff --git a/terraform/locals.tf b/terraform/locals.tf index 652c908..f6dc243 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -7,6 +7,11 @@ locals { github_oidc_provider_arn = "arn:aws:iam::${local.account_id}:oidc-provider/token.actions.githubusercontent.com" + # Prod has no default VPC. 10.60 is unused in 011934824531 + # (10.0 proposal-system, 10.20 payments-dashboard, 10.40 syslog, 10.80 apm-wo). + vpc_cidr = "10.60.0.0/16" + public_subnet_cidrs = ["10.60.0.0/24", "10.60.1.0/24"] + form_bucket_name = "${local.project}-form-${local.account_id}" reports_bucket_name = "${local.project}-reports-${local.account_id}" 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/test_terraform_vpc.py b/tests/test_terraform_vpc.py new file mode 100644 index 0000000..2e22439 --- /dev/null +++ b/tests/test_terraform_vpc.py @@ -0,0 +1,26 @@ +"""Meals owns a dedicated VPC. Prod has no default VPC.""" + +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TERRAFORM = ROOT / "terraform" + + +def _read(name: str) -> str: + return (TERRAFORM / name).read_text() + + +def test_meals_owns_a_vpc_instead_of_looking_up_default(): + vpc = _read("vpc.tf") + ecs = _read("ecs.tf") + locals_tf = _read("locals.tf") + + assert 'resource "aws_vpc" "this"' in vpc + assert "cidr_block = local.vpc_cidr" in vpc + assert 'vpc_cidr = "10.60.0.0/16"' in locals_tf + 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 _read("hcp_iam.tf")