Observing identity instead of counting objects
This lesson’s example uses a map with reports and payments as keys. Each key identifies a distinct terraform_data instance in state. After applying in a new directory, remove only reports and review the plan. payments retains the same address and value; reports is proposed for deletion. This observation is specific to the example’s attributes. A later change to the payments value may still cause an update or replacement depending on resource type. A stable key avoids identity shifts caused by position but does not prevent functional changes or replace inspection of proposed attributes.
Comparing count, for_each, and collection transformations
With count and a [reports,payments,funds] list, each instance uses an index. If input depends on local.names[count.index], removing reports shifts values at indices zero and one, while index two disappears. In the terraform_data lab, this produces two updates and one deletion. Do not generalize to every provider: some attributes may require replacement. for_each accepts maps or sets of strings and identifies instances by keys or elements. toset removes duplicates and does not preserve list order. If two entries represent different objects, do not accidentally collapse them into one string when choosing identity.
Separating known keys from pending values
Terraform needs to identify instances during planning. A set built from an ID that will only be created during apply does not provide that identity upfront. In the lab, for_each=toset([terraform_data.upstream.id]) fails. A map with a known primary key and the ID as its value allows the dependent address to be planned; the value remains pending until available. Unknown values are not automatically invalid in every argument. Distinguish identity needed to expand the graph from data consumed later. Also avoid passwords as keys: identifiers appear in displayed addresses and need to be nonsecret.
Declaring an address change
If reports becomes reporting, Terraform sees different addresses. When the intention is to retain the same object, declare a moved mapping between old and new addresses and inspect the plan. In the lab, renaming legacy to current with the appropriate declaration retains the object and updates its binding after apply. Other changes to the same resource still follow their own semantics; moved does not cancel every functional change. In published modules, retain migration paths needed by consumers skipping versions. Removing a moved block too early can remove information needed to upgrade an old state and cause unintended creation and deletion.
Module interfaces and proportionate dependencies
A child module publishes a result through an output. The root can consume it as module.service.endpoint and expose it again in its own output. Internal child locals do not automatically become accessible to the caller. Useful references also express graph relationships. A depends_on applied to an entire module can make more values unknown during planning when an upstream change exists. Before adding or removing the relationship, identify the real dependency: specific references are preferable when they correctly represent the need. Removing dependencies to obtain a visually simpler plan can allow an incorrect execution order.
Distinguishing removal, destruction, and ownership transfer
A removed block with destroy=false asks Terraform to stop managing the object in that state without requesting deletion. In the lab, the action appears as forget and the local binding disappears after apply. Because terraform_data does not represent remote infrastructure, that test demonstrates neither server survival nor migration to another team. During a real transfer, identify the destination, owner, adoption method, dependencies, and handover point. Avoid two states attempting to manage the same object simultaneously or a gap without an owner. Removing one binding does not automatically create another, and a state backup does not replace an application data backup.
terraform {
required_version = "~> 1.12.0"
}
locals {
records = {
reports = "r"
payments = "p"
}
}
resource "terraform_data" "entry" {
for_each = local.records
input = each.value
}
Apply the map in a new directory, remove reports, and observe payments unchanged. Then rename payments to settlement and compare plans with and without a moved mapping between complete addresses.
Common pitfalls
Using position as durable identity without assessing shifts; choosing unknown IDs as keys; exposing secrets in addresses; removing moved too early; confusing forget with completed migration.
Related topics: Modules and contracts · State management
A name change, position change, and ownership change have different effects. Preserve the relationship between intent, address, and object, and confirm each transition with a reviewed plan.
Reference: Refactor modules · 004