Symptom
An Azure stack using infrastructure/azure/vnet + infrastructure/azure/aks + infrastructure/azure/aks_route_table never reaches No changes. Every plan, forever:
Plan: 2 to add, 1 to change, 2 to destroy.
Applying it succeeds and leaves the cluster healthy — the route table ends up correctly attached — but the very next plan is byte-identical. Verified by applying and immediately re-planning:
$ tofu apply -auto-approve
Apply complete! Resources: 2 added, 1 changed, 2 destroyed.
$ tofu plan
Plan: 2 to add, 1 to change, 2 to destroy. # unchanged
Root cause — two modules own the same field
1. vnet wants to remove the route table. The AVM subnet resource manages the whole request body, and nothing in its config mentions a route table, so every plan proposes:
# module.vnet...azapi_resource.subnet[0] will be updated in-place
~ properties = {
~ routeTable = {
- id = ".../MC_<rg>_<cluster>_<region>/.../routeTables/aks-agentpool-...-routetable"
} -> null
That route table is created by AKS itself for kubenet clusters, after the vnet exists, so the vnet module has no way to know about it.
2. aks_route_table re-attaches it unconditionally. From infrastructure/azure/aks_route_table/main.tf:7:
triggers_replace = timestamp()
timestamp() differs on every plan, so terraform_data.trigger always replaces, and azapi_update_resource.aks_subnet_route_table follows it through replace_triggered_by.
The two cancel out on every apply and reset for the next one.
Why it matters
- The stack can never be used as a convergence check. Real drift is indistinguishable from this permanent noise, which is precisely how an unrelated bug went unnoticed for us: a stale registry hostname sat in a plan nobody could read as clean.
- Every apply detaches and re-attaches the route table on a live kubenet cluster. It is brief, but pod-to-pod traffic across nodes degrades in that window — a recurring cost paid on every unrelated change.
Suggested direction
Fixing only the trigger is not enough: even with a stable trigger, vnet still proposes routeTable -> null on every plan. Both halves need to move.
aks_route_table — derive triggers_replace from the values that actually matter (subnet id + route table id) instead of timestamp(), so it re-runs only when the attachment really changes.
vnet — stop fighting for the field: either expose a per-subnet route_table input, or add lifecycle { ignore_changes = [...routeTable] } on the subnet so an externally-managed route table survives.
Happy to send the PR once you decide which shape you prefer for (2) — exposing the input is cleaner but needs the id up front, which is awkward because AKS only creates the route table after the cluster comes up.
Environment
tofu-modules v6.8.1 · AKS kubenet (podCidr 10.244.0.0/16) · OpenTofu with azapi + azurerm.
Symptom
An Azure stack using
infrastructure/azure/vnet+infrastructure/azure/aks+infrastructure/azure/aks_route_tablenever reachesNo changes. Every plan, forever:Applying it succeeds and leaves the cluster healthy — the route table ends up correctly attached — but the very next plan is byte-identical. Verified by applying and immediately re-planning:
Root cause — two modules own the same field
1.
vnetwants to remove the route table. The AVM subnet resource manages the whole request body, and nothing in its config mentions a route table, so every plan proposes:That route table is created by AKS itself for kubenet clusters, after the vnet exists, so the vnet module has no way to know about it.
2.
aks_route_tablere-attaches it unconditionally. Frominfrastructure/azure/aks_route_table/main.tf:7:timestamp()differs on every plan, soterraform_data.triggeralways replaces, andazapi_update_resource.aks_subnet_route_tablefollows it throughreplace_triggered_by.The two cancel out on every apply and reset for the next one.
Why it matters
Suggested direction
Fixing only the trigger is not enough: even with a stable trigger,
vnetstill proposesrouteTable -> nullon every plan. Both halves need to move.aks_route_table— derivetriggers_replacefrom the values that actually matter (subnet id + route table id) instead oftimestamp(), so it re-runs only when the attachment really changes.vnet— stop fighting for the field: either expose a per-subnetroute_tableinput, or addlifecycle { ignore_changes = [...routeTable] }on the subnet so an externally-managed route table survives.Happy to send the PR once you decide which shape you prefer for (2) — exposing the input is cleaner but needs the id up front, which is awkward because AKS only creates the route table after the cluster comes up.
Environment
tofu-modules v6.8.1 · AKS kubenet (
podCidr 10.244.0.0/16) · OpenTofu withazapi+azurerm.