Skip to content
Infrastructure as Code iac state 4 min read

Sharing Remote State

Large infrastructure is rarely managed in a single Terraform configuration. Teams split it into separate stacks — a networking stack, a database stack, an application stack — each with its own state file and lifecycle. The terraform_remote_state data source lets one stack read the published outputs of another, so an app stack can wire itself to the VPC and subnets created by the network stack without hard-coding IDs. This sharing is powerful, but it creates a coupling you should understand before reaching for it.

How terraform_remote_state works

The terraform_remote_state data source connects to another configuration’s backend, reads its state file, and exposes that configuration’s root-level outputs under an outputs attribute. It never reads individual resource attributes — only what the producing stack explicitly published with output blocks. That output contract is the public API between stacks.

A typical split looks like a network stack that exports its VPC and subnet IDs:

# network/outputs.tf — the producing stack
output "vpc_id" {
  description = "ID of the shared VPC"
  value       = aws_vpc.main.id
}

output "private_subnet_ids" {
  description = "Private subnets for application workloads"
  value       = aws_subnet.private[*].id
}

The application stack then consumes those outputs by pointing the data source at the same backend the network stack writes to:

# app/main.tf — the consuming stack
data "terraform_remote_state" "network" {
  backend = "s3"

  config = {
    bucket = "acme-tf-state"
    key    = "network/terraform.tfstate"
    region = "us-east-1"
  }
}

resource "aws_instance" "api" {
  ami           = "ami-0abcdef1234567890"
  instance_type = "t3.medium"
  subnet_id     = data.terraform_remote_state.network.outputs.private_subnet_ids[0]

  tags = {
    Name = "api-server"
    Vpc  = data.terraform_remote_state.network.outputs.vpc_id
  }
}

The config block mirrors the producing stack’s backend configuration. For an S3 backend you supply the bucket, key, and region; the data source then performs a read against that object. This is read-only — the consuming stack never modifies the network state.

The data source resolves at plan time, before any resources are created. If the network stack hasn’t been applied yet, its outputs won’t exist and your plan will fail. Always apply producing stacks before their consumers.

Other backends

Any supported backend can be a source. For Terraform Cloud / HCP Terraform, reference the workspace directly:

data "terraform_remote_state" "network" {
  backend = "remote"

  config = {
    organization = "acme"
    workspaces = {
      name = "network-prod"
    }
  }
}

This works identically in OpenTofu — the terraform_remote_state data source and the backend protocol are shared, so the same configuration reads tofu-managed state without changes.

The coupling trade-off

terraform_remote_state creates a tight, implicit dependency: the consumer reads the producer’s entire state file and depends on the exact names of its outputs. That has real consequences.

ConcernImplication
Output stabilityRenaming or removing a producer output breaks every consumer’s plan.
State accessThe consumer needs read access to the producer’s full state, which may contain secrets in plaintext.
Apply orderingProducers must be applied before consumers; there is no automatic dependency graph across stacks.
Blast radiusCoupling two stacks reduces the isolation that splitting them was meant to provide.

Because the data source exposes the whole state, granting read access leaks any sensitive values stored there. Prefer narrowly scoped IAM policies on the state bucket, and treat outputs as a deliberately curated, stable interface — not an afterthought.

Alternatives to remote state sharing

Reading state is not the only way to discover another stack’s resources. Often a looser coupling is healthier:

  • Provider data sources — Query the live cloud API instead of another stack’s state. This works across tools and teams and needs no shared state access.
data "aws_vpc" "main" {
  tags = {
    Name = "acme-shared"
  }
}

data "aws_subnets" "private" {
  filter {
    name   = "vpc-id"
    values = [data.aws_vpc.main.id]
  }

  tags = {
    Tier = "private"
  }
}
  • SSM Parameter Store — The producer writes IDs to well-known parameter paths; consumers read them. This decouples the stacks entirely and works for non-Terraform consumers too.
# producer
resource "aws_ssm_parameter" "vpc_id" {
  name  = "/acme/network/vpc_id"
  type  = "String"
  value = aws_vpc.main.id
}

# consumer
data "aws_ssm_parameter" "vpc_id" {
  name = "/acme/network/vpc_id"
}

A plan that wires these together shows the resolved values inline:

Output:

  # aws_instance.api will be created
  + resource "aws_instance" "api" {
      + ami           = "ami-0abcdef1234567890"
      + instance_type = "t3.medium"
      + subnet_id     = "subnet-0a1b2c3d4e5f60718"
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Best Practices

  • Treat root outputs as a versioned public contract; never rename or remove them without coordinating with consumers.
  • Apply producing stacks before consuming stacks, and document the ordering in your pipeline.
  • Keep secrets out of outputs — remote state read access exposes the entire state file.
  • Restrict state-bucket read permissions to the specific keys a consumer actually needs.
  • Prefer provider data sources or SSM parameters when you only need a handful of IDs and want looser coupling.
  • Pin the source backend’s key/workspace explicitly so consumers can’t accidentally read the wrong environment.
  • Validate that outputs keys exist before referencing them, especially after refactoring the producer.
Last updated June 14, 2026
Was this helpful?