Terraform & IaC
Why this is necessary
Section titled “Why this is necessary”A resource can only have one owner. After onboarding:
- Default routes (
0.0.0.0/0in private subnets) point at the Outbound gateway, and our control plane reconciles them continuously. If Terraform still has the old route in state,applyreverts it to your old NAT Gateway → egress bypasses (or loses) its path → Outbound repairs it → the nextapplyreverts it again. In the worst case (the old NAT Gateway is gone), reverting the route breaks egress outright. - Elastic IPs: Outbound allocates your gateway EIPs and keeps a spare pool (tagged
do-not-delete-cloudphilos-gateway) that preserves your egress IP across replacements. If Terraform manages these, adestroyor drift-correction can release your static egress IP — it goes back to AWS and may be unrecoverable, breaking every allowlist that references it. - NAT Gateways: after cutover, Outbound deletes your old NAT Gateway so you stop paying for it. If it’s still in your Terraform state,
applyrecreates it — an idle NAT Gateway you pay AWS for while all traffic flows through Outbound — and your configuration and reality drift apart permanently.
What to remove
Section titled “What to remove”| Terraform resource | Which ones | Why |
|---|---|---|
aws_route with destination_cidr_block = "0.0.0.0/0" | In every Outbound-managed private subnet’s route table | Outbound owns and reconciles the default route. |
aws_eip | The EIP of any replaced NAT Gateway, and never import Outbound-tagged EIPs | Outbound pools these for the static-IP guarantee. |
aws_nat_gateway | Any NAT Gateway replaced by an Outbound gateway | Outbound deletes it after cutover. |
You can recognize every Outbound-managed resource by its tags: instances Name=cloudphilos-gateway, security groups cloudphilos-gateway-<vpc-id>, Elastic IPs Name=do-not-delete-cloudphilos-gateway. Never import these into Terraform, and never delete them manually — the EIP tag means exactly what it says.
How to remove them
Section titled “How to remove them”Option A — removed blocks (Terraform ≥ 1.7, recommended)
Section titled “Option A — removed blocks (Terraform ≥ 1.7, recommended)”Removes the resources from state declaratively, without destroying them, and keeps the removal in code review:
removed { from = aws_route.private_default lifecycle { destroy = false }}
removed { from = aws_nat_gateway.main lifecycle { destroy = false }}
removed { from = aws_eip.nat lifecycle { destroy = false }}Also delete (or comment out) the corresponding resource blocks, then run terraform plan — it should show only “will no longer be managed” removals, no destroys.
Option B — terraform state rm
Section titled “Option B — terraform state rm”terraform state rm 'aws_route.private_default'terraform state rm 'aws_nat_gateway.main'terraform state rm 'aws_eip.nat'
# with count/for_each, quote the index:terraform state rm 'aws_route.private_default["eu-west-1a"]'Then delete the resource blocks from your configuration so a later apply doesn’t recreate them.
If your routes are inline route {} blocks
Section titled “If your routes are inline route {} blocks”Inline routes on an aws_route_table can’t be removed from state individually. Either migrate them to standalone aws_route resources first (preferred — it makes ownership explicit), or tell Terraform to leave the whole route set alone:
resource "aws_route_table" "private" { vpc_id = aws_vpc.main.id
lifecycle { ignore_changes = [route] }}ignore_changes = [route] means Terraform ignores all route drift on that table, including routes you still consider yours — acceptable for tables whose only variable route is the default route, otherwise prefer the migration.
Modules you don’t fully control
Section titled “Modules you don’t fully control”If your VPC comes from a shared module (e.g. terraform-aws-modules/vpc), the NAT gateway/EIP/route resources live inside it. Set the module’s inputs so it stops declaring them (e.g. enable_nat_gateway = false after cutover) and use removed blocks / state rm against the module-qualified addresses:
terraform state rm 'module.vpc.aws_nat_gateway.this[0]'terraform state rm 'module.vpc.aws_eip.nat[0]'terraform state rm 'module.vpc.aws_route.private_nat_gateway[0]'Verifying you’re clean
Section titled “Verifying you’re clean”After the cleanup, run:
terraform planThe plan must show no changes to default routes, NAT gateways, or EIPs in Outbound-managed VPCs. If it proposes replacing a route target or creating a NAT Gateway, something is still in state — resolve it before applying anything.
CI guardrail (recommended)
Section titled “CI guardrail (recommended)”Teams with automated applies should add a plan check that fails on changes to the protected resources, for example by grepping terraform show -json plan.out for aws_route, aws_nat_gateway, or aws_eip changes in managed VPCs. It turns an outage into a failed pipeline.