diff --git a/terraform/data.tf b/terraform/data.tf index 859daee..33df82e 100644 --- a/terraform/data.tf +++ b/terraform/data.tf @@ -68,3 +68,26 @@ check "dev_has_no_custom_domain" { error_message = "attach_custom_domain must be false in non-prod; use the CloudFront distribution domain." } } + +check "existing_vpc_pair" { + assert { + condition = (var.existing_vpc_id == "") == (length(var.existing_public_subnet_ids) == 0) + error_message = "existing_vpc_id and existing_public_subnet_ids must both be set or both be empty." + } +} + +check "existing_vpc_two_az" { + assert { + condition = var.existing_vpc_id == "" || length(var.existing_public_subnet_ids) >= 2 + error_message = "existing_public_subnet_ids must include at least two subnets." + } +} + +check "existing_subnets_in_vpc" { + assert { + condition = alltrue([ + for subnet in data.aws_subnet.existing_public : subnet.vpc_id == var.existing_vpc_id + ]) + error_message = "Every existing_public_subnet_ids value must belong to existing_vpc_id." + } +} diff --git a/terraform/ecs.tf b/terraform/ecs.tf index d15fecc..7f6018a 100644 --- a/terraform/ecs.tf +++ b/terraform/ecs.tf @@ -39,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 = aws_vpc.this.id + vpc_id = local.vpc_id ingress { # CloudFront prefix lists cannot cover GitHub-hosted weekly-menu HMAC @@ -63,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 = aws_vpc.this.id + vpc_id = local.vpc_id ingress { description = "From ALB" @@ -86,7 +86,7 @@ resource "aws_lb" "api" { load_balancer_type = "application" idle_timeout = 120 security_groups = [aws_security_group.alb.id] - subnets = aws_subnet.public[*].id + subnets = local.public_subnet_ids drop_invalid_header_fields = true } @@ -95,7 +95,7 @@ resource "aws_lb_target_group" "api" { name = "${local.project}-api" port = 8080 protocol = "HTTP" - vpc_id = aws_vpc.this.id + vpc_id = local.vpc_id target_type = "ip" health_check { @@ -202,7 +202,7 @@ resource "aws_ecs_service" "api" { launch_type = "FARGATE" network_configuration { - subnets = aws_subnet.public[*].id + subnets = local.public_subnet_ids security_groups = [aws_security_group.api.id] assign_public_ip = true } diff --git a/terraform/locals.tf b/terraform/locals.tf index f6dc243..a667f37 100644 --- a/terraform/locals.tf +++ b/terraform/locals.tf @@ -7,8 +7,8 @@ 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). + # Created only when existing_vpc_id is empty. 10.60 is unused in 011934824531. + manage_vpc = var.existing_vpc_id == "" vpc_cidr = "10.60.0.0/16" public_subnet_cidrs = ["10.60.0.0/24", "10.60.1.0/24"] diff --git a/terraform/variables.tf b/terraform/variables.tf index 90d95b9..e057094 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -91,3 +91,15 @@ variable "checkcomponents_queue_arn" { type = string default = "arn:aws:sqs:us-east-1:011934824531:paychex-checkcomponents" } + +variable "existing_vpc_id" { + description = "When set, place the ALB and Fargate tasks in this VPC instead of creating one." + type = string + default = "" +} + +variable "existing_public_subnet_ids" { + description = "Public subnet IDs in existing_vpc_id. Required with existing_vpc_id; ignored when that variable is empty." + type = list(string) + default = [] +} diff --git a/terraform/vpc.tf b/terraform/vpc.tf index f59a22e..8c28033 100644 --- a/terraform/vpc.tf +++ b/terraform/vpc.tf @@ -1,8 +1,21 @@ data "aws_availability_zones" "available" { + count = local.manage_vpc ? 1 : 0 state = "available" } +data "aws_vpc" "existing" { + count = local.manage_vpc ? 0 : 1 + id = var.existing_vpc_id +} + +data "aws_subnet" "existing_public" { + for_each = toset(var.existing_public_subnet_ids) + id = each.value +} + resource "aws_vpc" "this" { + count = local.manage_vpc ? 1 : 0 + cidr_block = local.vpc_cidr enable_dns_support = true enable_dns_hostnames = true @@ -19,7 +32,9 @@ resource "aws_vpc" "this" { } resource "aws_internet_gateway" "this" { - vpc_id = aws_vpc.this.id + count = local.manage_vpc ? 1 : 0 + + vpc_id = aws_vpc.this[0].id tags = { Name = "${local.project}-igw" @@ -27,11 +42,11 @@ resource "aws_internet_gateway" "this" { } resource "aws_subnet" "public" { - count = length(local.public_subnet_cidrs) + count = local.manage_vpc ? length(local.public_subnet_cidrs) : 0 - vpc_id = aws_vpc.this.id + vpc_id = aws_vpc.this[0].id cidr_block = local.public_subnet_cidrs[count.index] - availability_zone = data.aws_availability_zones.available.names[count.index] + availability_zone = data.aws_availability_zones.available[0].names[count.index] map_public_ip_on_launch = true tags = { @@ -40,7 +55,9 @@ resource "aws_subnet" "public" { } resource "aws_route_table" "public" { - vpc_id = aws_vpc.this.id + count = local.manage_vpc ? 1 : 0 + + vpc_id = aws_vpc.this[0].id tags = { Name = "${local.project}-public" @@ -48,14 +65,46 @@ resource "aws_route_table" "public" { } resource "aws_route" "public_default" { - route_table_id = aws_route_table.public.id + count = local.manage_vpc ? 1 : 0 + + route_table_id = aws_route_table.public[0].id destination_cidr_block = "0.0.0.0/0" - gateway_id = aws_internet_gateway.this.id + gateway_id = aws_internet_gateway.this[0].id } resource "aws_route_table_association" "public" { - count = length(local.public_subnet_cidrs) + count = local.manage_vpc ? length(local.public_subnet_cidrs) : 0 subnet_id = aws_subnet.public[count.index].id - route_table_id = aws_route_table.public.id + route_table_id = aws_route_table.public[0].id +} + +locals { + vpc_id = local.manage_vpc ? aws_vpc.this[0].id : data.aws_vpc.existing[0].id + public_subnet_ids = local.manage_vpc ? aws_subnet.public[*].id : var.existing_public_subnet_ids +} + +moved { + from = aws_vpc.this + to = aws_vpc.this[0] +} + +moved { + from = aws_internet_gateway.this + to = aws_internet_gateway.this[0] +} + +moved { + from = aws_route_table.public + to = aws_route_table.public[0] +} + +moved { + from = aws_route.public_default + to = aws_route.public_default[0] +} + +moved { + from = data.aws_availability_zones.available + to = data.aws_availability_zones.available[0] } diff --git a/tests/test_terraform_vpc.py b/tests/test_terraform_vpc.py index 2e22439..cf050aa 100644 --- a/tests/test_terraform_vpc.py +++ b/tests/test_terraform_vpc.py @@ -1,4 +1,4 @@ -"""Meals owns a dedicated VPC. Prod has no default VPC.""" +"""Meals creates a VPC unless existing_vpc_id is set (prod shares afterhours).""" from pathlib import Path @@ -14,13 +14,22 @@ def test_meals_owns_a_vpc_instead_of_looking_up_default(): vpc = _read("vpc.tf") ecs = _read("ecs.tf") locals_tf = _read("locals.tf") + variables = _read("variables.tf") + data = _read("data.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 "count = local.manage_vpc ? 1 : 0" in vpc + assert 'variable "existing_vpc_id"' in variables + assert 'variable "existing_public_subnet_ids"' in variables + assert "manage_vpc = var.existing_vpc_id == \"\"" 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 "vpc_id = local.vpc_id" in ecs + assert "subnets = local.public_subnet_ids" in ecs + assert "subnets = local.public_subnet_ids" in ecs assert "ec2:CreateVpc" in _read("hcp_iam.tf") + assert 'check "existing_vpc_pair"' in data + assert 'check "existing_subnets_in_vpc"' in data + assert "from = aws_vpc.this" in vpc + assert "to = aws_vpc.this[0]" in vpc