OnCallReady

Lesson 13.30 · Terraform: State & Modules · 17 min read

Across states: sharing values, and moving objects between states

In plain words

Two neighbours each keep their own house notebook. The network family knows the address of the shared gate; the app family needs to know it too. They can read the network family's whole notebook (including their diary), or just walk to the gate and read the sign on it. And if one family gives the garden shed to the other, the first writes "no longer ours, but don't knock it down" and the second writes "now ours", on the same afternoon.

In Terraform, separate states share values with terraform_remote_state (read another state's outputs, but you need read on the whole blob), data sources on the real objects by name (data "azurerm_subnet"), or a neutral store like App Configuration. Moving an object between states is a removed block with destroy = false in one, then an import block in the other.

Why you end up with several states

The platform team's network lives in one state; your app lives in another (13.1 argued for that split). Now your app's VM needs the subnet ID - which only the network state knows. And next quarter a subnet has to move from the network state into yours, without being destroyed on the way.

Split states raise exactly these two questions:

  1. How does one configuration use something another one manages?
  2. How do you move an object from one state to another without destroying it?

What you need to know already:

Reading another state's values

Option 1: terraform_remote_state

The network configuration publishes outputs:

# network/outputs.tf
output "subnet_ids" {
  value = { for k, s in azurerm_subnet.this : k => s.id }
}

That for expression (12.11) builds a map from subnet key to subnet ID.

The app configuration reads them with a special data source, terraform_remote_state. It takes the same settings as the other state's backend block, and exposes that state's outputs under .outputs:

data "terraform_remote_state" "network" {
  backend = "azurerm"
  config = {
    resource_group_name  = "rg-tfstate"
    storage_account_name = "sttfstatesysop"
    container_name       = "tfstate"
    key                  = "network/dev.tfstate"
  }
}

resource "azurerm_linux_virtual_machine" "app" {
  # ...
  subnet_id = data.terraform_remote_state.network.outputs.subnet_ids["app"]
}

What to know about it:

If the state does not exist yet (network not applied, wrong key), plan fails:

│ Error: Unable to find remote state
│
│ No stored state was found for the given workspace in the given backend.

Option 2: data sources on the real objects

Instead of reading the other team's state, look the subnet up in Azure by name (12.18):

data "azurerm_subnet" "app" {
  name                 = "snet-app"
  virtual_network_name = "vnet-spoke-orders-dev"
  resource_group_name  = "rg-network-dev"
}

resource "azurerm_linux_virtual_machine" "app" {
  subnet_id = data.azurerm_subnet.app.id
}

HashiCorp's own guidance prefers this in most cases: put stable facts in the platform (names, tags) and look them up with data sources.

Option 3: publish values somewhere neutral

The producer writes its values into a shared store - for example Azure App Configuration (a service that holds key/value settings) or a Key Vault - as resources in its own configuration. Consumers read them with data sources. More moving parts, but the two sides no longer depend on each other's code, and it works across tools.

remote_state     simplest; needs read on the whole state; tight coupling on output names
data sources     no state access; couples on names; works across tools   <- default
neutral store    most decoupled; more to build

Moving an object between states

terraform state mv (13.15) only works inside one state. With a remote backend there is no move between states. You do it in two reviewed steps, and the order matters.

Step 1 - the source configuration lets go. Delete the resource block and put a removed block in its place. removed says "forget this address"; destroy = false says "and do not delete the real object":

# network/main.tf: the block is deleted and replaced by
removed {
  from = azurerm_subnet.batch

  lifecycle {
    destroy = false
  }
}

The plan says so in plain words:

  # azurerm_subnet.batch will no longer be managed by Terraform, but will not be destroyed
╷
│ Warning: Some objects will no longer be managed by Terraform
│
│ If you apply this plan, Terraform will discard its tracking information for
│ the following objects, but it will not delete them:
│  - azurerm_subnet.batch
╵

Apply. The subnet still exists; no state mentions it.

Step 2 - the destination configuration adopts it with an import block (13.25) and a matching resource block:

# batch/main.tf
import {
  to = azurerm_subnet.batch
  id = "/subscriptions/.../virtualNetworks/vnet-spoke/subnets/batch"
}

resource "azurerm_subnet" "batch" {
  name                 = "batch"
  resource_group_name  = "rg-network-dev"
  virtual_network_name = "vnet-spoke"
  address_prefixes     = ["10.41.9.0/24"]
}
Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.

Apply. Now exactly one state manages it.

Rules for doing this safely:

terraform state rm in the source does the same as step 1, but nobody can review it. Before Terraform 1.7 (no removed block) it was the only way.

Moving everything: splitting a state

Splitting a big state - moving the whole network out of an "everything" state - is the same two steps for many objects:

  1. New configuration network/ with the resource blocks and one import block per object (or a for_each import). Plan - expect N to import, 0 to change. Do not apply yet.
  2. In the old configuration, replace those blocks with removed { ... destroy = false }, and turn references to the network (azurerm_subnet.app.id) into data sources or terraform_remote_state reads. Plan - expect N no longer managed, 0 to destroy, and no changes to anything that used them.
  3. Apply the old configuration, then straight away the new one.
  4. Plan both. Both clean.

A state split is a change like any other: it goes through PRs and pipelines, and nobody applies anything else to those states while it is in flight.

What you can now do:

Why it helps

Once states are split, the app team needs the subnet ID from the platform team's network state, and your choice decides coupling and security. Granting the app pipeline read on the network state blob hands it every secret in that state, which is the kind of finding a security review will make. When a team reorganises and the batch subnet moves to the batch team, doing removed plus import in the right order, in one window, avoids both a destroyed subnet and two states fighting over it. Splitting a monolith state is a typical senior platform ticket and an interview question.

FAQ

Why not always use terraform_remote_state? It is the simplest.

It is simple, but the reader needs read access to the whole state blob, which contains every attribute of every resource, secrets included. It only sees root-module outputs, and it couples consumers to output names, so renaming an output breaks the consumer's next plan. Data sources on real objects need only read access to those objects in Azure, couple on names a convention already stabilises, and work whatever tool manages the producer.

Can I use terraform state mv between two remote states?

No. state mv works within one state. Between states you use two reviewed steps: in the source configuration, replace the block with removed { from = ... lifecycle { destroy = false } } and apply; in the destination, add the resource block plus an import block and apply. Do both in one change window, and never let both states manage the object at the same time.

What should the plans look like when moving an object between states?

The source plan should say the object "will no longer be managed by Terraform, but will not be destroyed", with 0 to destroy. The destination plan should say "1 to import, 0 to add, 0 to change, 0 to destroy". Any change in the destination means the new block does not match reality; fix it before applying.

What goes wrong if both states manage the same object?

Each has its own lock, so nothing stops them. Each plan sees the other's changes as drift and reverts them, so the resource flip-flops on every apply. If one of them deletes its block without removed, it destroys the object the other still relies on. One object, one owner, always.

What is the neutral store option?

The producer writes values it wants to share (subnet IDs, workspace IDs) as resources in Azure App Configuration or Key Vault; consumers read them with data sources. It decouples completely: consumers need no state access, the producer can refactor freely, and it works across tools like Bicep or Pulumi. The cost is more moving parts to build and keep current.

In an interview Mid

How do you share values between Terraform configurations that have separate states?

Three options:

Moving an object between states is different: a removed block with destroy = false in the source, then an import block in the destination, both in one change window, and the destination plan must say 1 to import, 0 to change.

Also asked: Another team needs the ID of a subnet your Terraform manages. How do you give it to them? · How would you split a large Terraform state into smaller ones? · Why should two states never manage the same object?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.