Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.


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_public
  • aws_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

  1. grep for aws_route_table resources that contain an inline route { block.
  2. Cross-check for standalone aws_route resources whose route_table_id = aws_route_table.<same> points at that table.
  3. 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.