Terraform Route Table: Inline vs Standalone Routes (prod gotcha)
A real Terraform production gotcha debugged and fixed in hungryhub-terraform,
module vpc/. Documented per the “document after debugging” convention.
- Repo: hungryhub-team/hungryhub-terraform
- Module:
vpc/— filevpc/main.tf - Fix shipped: PR #381 (Aiven prod-only refactor: PR #380; related issue #285)
The bug class
An aws_route_table with an inline route { ... } block is authoritative over its
entire route set. If you also manage routes on that same table via standalone
aws_route resources, every terraform apply makes the route table reconcile its inline
set to exactly what is declared inline — deleting the standalone routes — and then the
standalone aws_route resources re-add them on the next run. The result is a perpetual
fight between the two, plus a real window where prod routing drops.
The fix
Add a lifecycle { ignore_changes = [route] } block to the aws_route_table. The inline
routes are then frozen as-is and the standalone aws_route resources own the rest.
Rule of thumb: go fully inline OR fully standalone + ignore_changes — never mix
without ignore_changes.
resource "aws_route_table" "route_tables_public" {
vpc_id = aws_vpc.vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
# Aiven/Databricks peering routes are managed as standalone aws_route
# resources; ignore inline route drift so TF does not fight them.
lifecycle {
ignore_changes = [route]
}
}
Real case
Tables affected (both in vpc/main.tf):
aws_route_table.route_tables_publicaws_route_table.route_tables_db_prod
Both had inline route {} blocks (NAT 0.0.0.0/0 + Aiven 192.168.0.0/24) and
standalone aws_route resources targeting them:
aiven_route_for_route_tables_public, db_to_aiven_prod, and db_to_databricks_prod.
Symptom on the prod plan: route_tables_db_prod[1] / [2] showed an in-place
~ route rewrite that removed the Databricks 10.206.0.0/16 route (owned by the
standalone db_to_databricks_prod[1] / [2]). In other words, apply would delete a live
prod DB→Databricks peering route and then drift.
The fix in PR #381 added lifecycle { ignore_changes = [route] } to both
route_tables_public and route_tables_db_prod, leaving the standalone aws_route
resources as the single owner of the peering routes.
Second gotcha (found the same day): terraform import evaluates the full config
terraform import does a full config evaluation. A resource with an unresolvable
count blocks the import for any target, not just that resource.
Example that broke the import:
# count cannot resolve on prod because route_tables_db does not exist there
count = var.environment == "prod" ? length(aws_route_table.route_tables_db) : 0
Terraform throws Invalid count argument and aborts the import.
Fix: make the count statically determinable. For the dev-only resource
db_to_databricks_dev, set count = 0 so it never references a non-existent resource on
prod.
How to detect
grepforaws_route_tableresources that contain an inlineroute {block.- Cross-check for standalone
aws_routeresources whoseroute_table_id = aws_route_table.<same>points at that table. - Verify the table has
lifecycle { ignore_changes = [route] }.
# tables with inline routes
grep -n "aws_route_table\|route {" vpc/main.tf
# standalone routes and their target tables
grep -n "resource \"aws_route\"\|route_table_id" vpc/main.tf
# confirm the guard is present
grep -n "ignore_changes = \[route\]" vpc/main.tf
If a table is targeted by a standalone aws_route but is missing
ignore_changes = [route], it is at risk — add the lifecycle block.