Skip to content

Terraform & IaC

A resource can only have one owner. After onboarding:

  • Default routes (0.0.0.0/0 in private subnets) point at the Outbound gateway, and our control plane reconciles them continuously. If Terraform still has the old route in state, apply reverts it to your old NAT Gateway → egress bypasses (or loses) its path → Outbound repairs it → the next apply reverts 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, a destroy or 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, apply recreates it — an idle NAT Gateway you pay AWS for while all traffic flows through Outbound — and your configuration and reality drift apart permanently.
Terraform resourceWhich onesWhy
aws_route with destination_cidr_block = "0.0.0.0/0"In every Outbound-managed private subnet’s route tableOutbound owns and reconciles the default route.
aws_eipThe EIP of any replaced NAT Gateway, and never import Outbound-tagged EIPsOutbound pools these for the static-IP guarantee.
aws_nat_gatewayAny NAT Gateway replaced by an Outbound gatewayOutbound 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.

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.

Terminal window
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.

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.

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:

Terminal window
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]'

After the cleanup, run:

Terminal window
terraform plan

The 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.

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.