Skip to content

Commit f499603

Browse files
sir-sigurdclaude
andcommitted
Address review follow-up: egress-IP doc, negative-path tests, cleanups
- README: note that toggling the mode changes the stack's public egress IP (NAT EIPs released on disable; egress via the TGW's NAT when enabled), so anything allowlisting Quilt's egress address must be updated or it breaks silently. - examples/main.tf: align the IPv6 comment with the corrected mechanism (off = no IPv6 default route, clients use IPv4) instead of "IPv4 fallback". - vpc tests: add negative-path runs — enabled-without-id (attachment precondition), malformed id (variable validation), and id-set-without-enable (no-op: no attachment/routes). - vpc main.tf: hoist the repeated `new_network_valid && enable_transit_gateway` count gate into a `transit_gateway_enabled` local; add a comment on why the attachment lands in intra subnets (ENI placement only). - quilt: align transit_gateway_id description with the vpc module (the new-VPC scoping lives on enable_transit_gateway). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 91645b6 commit f499603

5 files changed

Lines changed: 92 additions & 9 deletions

File tree

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,12 @@ delay.
823823
**Reversibility:** removing `enable_transit_gateway` (or setting it `false`)
824824
restores the NAT gateways and IPv6 egress-only IGW. Toggling it on or off for an
825825
already-deployed VPC recreates/destroys NAT gateways and their Elastic IPs and
826-
briefly interrupts egress, so do it in a maintenance window.
826+
briefly interrupts egress, so do it in a maintenance window. Either direction
827+
also **changes the stack's public egress IP** — disabling releases the NAT
828+
Elastic IPs (AWS won't hand the same ones back), and enabling sends egress out
829+
through the TGW's NAT instead — so anything that allowlists Quilt's egress
830+
address (a license endpoint, a partner firewall, a SaaS IP allowlist) must be
831+
updated, or it breaks silently.
827832

828833
### Profile
829834
You may wish to set a specific AWS profile before executing `terraform`

examples/main.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ module "quilt" {
139139
# api_endpoint = "vpce-YOUR-VPC-ENDPOINT" # VPC endpoint (if internal = true)
140140
# enable_transit_gateway = true # Route private-subnet egress via a TGW instead of NAT (create_new_vpc = true only)
141141
# transit_gateway_id = "tgw-YOUR-TRANSIT-GATEWAY-ID" # Required when enable_transit_gateway = true; the TGW must route to the internet and back
142-
# transit_gateway_ipv6_egress = true # Only if the TGW carries IPv6 egress (otherwise IPv6 is left on IPv4 fallback)
142+
# transit_gateway_ipv6_egress = true # Only if the TGW carries IPv6 egress; off = no IPv6 default route (clients use IPv4)
143143

144144
# CloudFormation notifications (optional)
145145
# stack_notification_arns = ["arn:aws:sns:YOUR-AWS-REGION:YOUR-ACCOUNT-ID:quilt-notifications"]

modules/quilt/variables.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ variable "enable_transit_gateway" {
3838
variable "transit_gateway_id" {
3939
type = string
4040
default = null
41-
description = "Transit Gateway ID for private subnet egress when creating a new VPC. Required when enable_transit_gateway == true; may be a computed value (e.g. a TGW created in the same configuration)."
41+
description = "Transit Gateway ID for private subnet egress. Required when enable_transit_gateway == true; may be a computed value (e.g. a TGW created in the same configuration)."
4242
validation {
4343
condition = var.transit_gateway_id == null || can(regex("^tgw-[0-9a-f]+$", var.transit_gateway_id))
4444
error_message = "transit_gateway_id must be null or a valid Transit Gateway ID (e.g. tgw-0123456789abcdef0)."

modules/vpc/main.tf

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ locals {
3333
new_network_valid = alltrue(values(local.new_network_requires))
3434
configuration_error = !local.existing_network_valid && !local.new_network_valid
3535

36+
# TGW egress is gated on the bool (not transit_gateway_id != null) so the
37+
# resource counts stay known at plan time even when transit_gateway_id is a
38+
# computed value (e.g. a TGW created in the same configuration).
39+
transit_gateway_enabled = local.new_network_valid && var.enable_transit_gateway
40+
3641
azs = slice(data.aws_availability_zones.available.names, 0, 2)
3742
subnet_cidrs = [for k, v in local.azs : cidrsubnet(var.cidr, 1, k)]
3843
}
@@ -78,11 +83,11 @@ module "vpc" {
7883
}
7984

8085
resource "aws_ec2_transit_gateway_vpc_attachment" "egress" {
81-
# Gate on the bool, not on transit_gateway_id != null: count must be known at
82-
# plan time, and transit_gateway_id may be a computed value (e.g. a TGW
83-
# created in the same configuration).
84-
count = local.new_network_valid && var.enable_transit_gateway ? 1 : 0
86+
count = local.transit_gateway_enabled ? 1 : 0
8587

88+
# Intra subnets only host the attachment ENIs (they have no internet route).
89+
# The egress default routes go in the private route tables below — don't move
90+
# this to private_subnets.
8691
subnet_ids = module.vpc.intra_subnets
8792
transit_gateway_id = var.transit_gateway_id
8893
vpc_id = module.vpc.vpc_id
@@ -101,7 +106,7 @@ resource "aws_ec2_transit_gateway_vpc_attachment" "egress" {
101106
}
102107

103108
resource "aws_route" "private_tgw_ipv4_egress" {
104-
count = local.new_network_valid && var.enable_transit_gateway ? length(module.vpc.private_route_table_ids) : 0
109+
count = local.transit_gateway_enabled ? length(module.vpc.private_route_table_ids) : 0
105110

106111
route_table_id = module.vpc.private_route_table_ids[count.index]
107112
destination_cidr_block = "0.0.0.0/0"
@@ -111,7 +116,7 @@ resource "aws_route" "private_tgw_ipv4_egress" {
111116
}
112117

113118
resource "aws_route" "private_tgw_ipv6_egress" {
114-
count = local.new_network_valid && var.enable_transit_gateway && var.transit_gateway_ipv6_egress ? length(module.vpc.private_route_table_ids) : 0
119+
count = local.transit_gateway_enabled && var.transit_gateway_ipv6_egress ? length(module.vpc.private_route_table_ids) : 0
115120

116121
route_table_id = module.vpc.private_route_table_ids[count.index]
117122
destination_ipv6_cidr_block = "::/0"

modules/vpc/tests/validation.tftest.hcl

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,76 @@ run "existing_vpc_with_transit_gateway_is_rejected" {
293293
# ignore the request.
294294
expect_failures = [output.configuration_error]
295295
}
296+
297+
run "transit_gateway_enabled_without_id_is_rejected" {
298+
command = plan
299+
300+
variables {
301+
create_new_vpc = true
302+
internal = false
303+
enable_transit_gateway = true
304+
transit_gateway_id = null
305+
existing_vpc_id = null
306+
existing_api_endpoint = null
307+
existing_intra_subnets = null
308+
existing_private_subnets = null
309+
existing_public_subnets = null
310+
existing_user_security_group = null
311+
existing_user_subnets = null
312+
}
313+
314+
# enable_transit_gateway = true requires a transit_gateway_id; the attachment
315+
# precondition must reject a null id.
316+
expect_failures = [aws_ec2_transit_gateway_vpc_attachment.egress]
317+
}
318+
319+
run "transit_gateway_id_invalid_format_is_rejected" {
320+
command = plan
321+
322+
variables {
323+
create_new_vpc = true
324+
internal = false
325+
enable_transit_gateway = true
326+
transit_gateway_id = "not-a-tgw-id"
327+
existing_vpc_id = null
328+
existing_api_endpoint = null
329+
existing_intra_subnets = null
330+
existing_private_subnets = null
331+
existing_public_subnets = null
332+
existing_user_security_group = null
333+
existing_user_subnets = null
334+
}
335+
336+
# A malformed transit_gateway_id must be rejected by the variable validation.
337+
expect_failures = [var.transit_gateway_id]
338+
}
339+
340+
run "transit_gateway_id_without_enable_is_noop" {
341+
command = plan
342+
343+
variables {
344+
create_new_vpc = true
345+
internal = false
346+
enable_transit_gateway = false
347+
transit_gateway_id = "tgw-00000000000000000"
348+
existing_vpc_id = null
349+
existing_api_endpoint = null
350+
existing_intra_subnets = null
351+
existing_private_subnets = null
352+
existing_public_subnets = null
353+
existing_user_security_group = null
354+
existing_user_subnets = null
355+
}
356+
357+
# transit_gateway_id is the value, enable_transit_gateway is the toggle: an id
358+
# set without enabling the mode is a no-op — no attachment, no TGW routes.
359+
assert {
360+
condition = length(aws_ec2_transit_gateway_vpc_attachment.egress) == 0
361+
error_message = "No TGW attachment should be created when enable_transit_gateway is false"
362+
}
363+
364+
assert {
365+
condition = length(aws_route.private_tgw_ipv4_egress) == 0 && length(aws_route.private_tgw_ipv6_egress) == 0
366+
error_message = "No TGW egress routes should be created when enable_transit_gateway is false"
367+
}
368+
}

0 commit comments

Comments
 (0)