terraform_remote_state: Reading State Across Workspaces

Quick Primer: What Is a Workspace?

A Terraform workspace is an isolated execution context with its own state file. Think of it as a boundary: everything declared in this directory of .tf files, backed by this state file, is one unit of infrastructure.

Most teams split their infrastructure into multiple workspaces — one for networking (VPCs, subnets), one for compute (instances, load balancers), one for databases. Each is applied and managed independently. Small blast radius, clear ownership. That’s the goal.

CLI workspaces let you create multiple named workspaces from the same config, each with its own state. HCP Terraform workspaces expand on this with remote execution, teams, and policies. For now, the definition above is enough.

The Problem: Separate Workspaces Don’t Talk to Each Other

You split your infrastructure into separate workspaces. Good practice. Networking is over here, compute is over there. Separate state files, separate terraform apply cycles. Clean.

But then your compute workspace needs the VPC ID and subnet IDs from networking. How does it get them?

You could hardcode the values. Copy the VPC ID from the console, paste it into your compute config. Of course, the moment networking recreates that VPC (maybe the CIDR changes), your hardcoded ID is wrong and compute breaks silently.

Or you use terraform_remote_state and read the values directly from the networking workspace’s state file. That’s the right answer.

terraform_remote_state — The Cross-Workspace Data Source

This is a data source that reads the output values of another workspace’s state file.

# In your networking workspace (produces outputs)
# networking/outputs.tf
output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*].id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}
# In your compute workspace (consumes networking outputs)
data "terraform_remote_state" "networking" {
  backend = "s3"

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

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.medium"
  subnet_id     = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
  vpc_security_group_ids = [aws_security_group.app.id]
}

The access pattern: data.terraform_remote_state.networking.outputs.<output_name>. Straightforward.

Supported Backends

terraform_remote_state works with any backend. The backend argument matches the backend type of the source workspace:

# S3 backend
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "company-tf-state"
    key    = "prod/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

# GCS backend
data "terraform_remote_state" "networking" {
  backend = "gcs"
  config = {
    bucket = "company-tf-state"
    prefix = "prod/networking"
  }
}

# HCP Terraform / Terraform Cloud backend
data "terraform_remote_state" "networking" {
  backend = "remote"
  config = {
    organization = "my-company"
    workspaces = {
      name = "networking-prod"
    }
  }
}
What Gets Exposed — Only Outputs

This is the part people miss: terraform_remote_state only exposes outputs from the source workspace. You can’t reach into the raw state and grab individual resource attributes.

# This works — reads an output
data.terraform_remote_state.networking.outputs.vpc_id

# This does NOT work — cannot access raw state resources
data.terraform_remote_state.networking.resources.aws_vpc.main.id  # INVALID

So if you need a value shared across workspaces, it must be declared as an output in the source. Plan your outputs upfront. I’ve lost count of how many times I’ve seen someone try to read a remote state, get confused by what’s available, and realize they forgot to export the value in the first place.

Access Control Implications

The workspace reading the remote state needs read access to the source state file. For S3, that means the IAM role/user needs s3:GetObject on the state key.

This is a real security boundary. If a workspace can read another’s state, it can read ALL its outputs — including sensitive ones. Even outputs marked as sensitive = true are fully accessible in plaintext through terraform_remote_state. The sensitive flag just suppresses display in plan output; it doesn’t encrypt anything in state. Don’t let that word give you a false sense of security.

Design your workspace boundaries and IAM policies with this in mind. If the security team’s state file contains KMS key ARNs, IAM policy documents, and OIDC thumbprints, anyone who can read that state file sees everything.

The Alternative: Data Sources for Shared Resources

For certain resources — VPCs, subnets, security groups — you don’t necessarily need terraform_remote_state. Cloud-native data sources with tags work too:

# Instead of remote_state, look up by tag
data "aws_vpc" "main" {
  tags = { Environment = "prod", ManagedBy = "networking-terraform" }
}

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

When to use which:

ApproachUse When
terraform_remote_stateYou control both workspaces; need complex values not expressible as tags
Cloud data sources (tags)Resources have stable, unique tags; teams are independent; you want loose coupling

terraform_remote_state creates a tight coupling between workspaces. If the networking team changes their output names, the compute workspace breaks. Data sources with tags are more resilient — the compute workspace finds what it needs based on the tags, independent of how the networking team structures their Terraform.

That said, I’ve used terraform_remote_state extensively in orgs where both workspaces are owned by the same platform team. The tight coupling is fine when one team owns both sides. When crossing team boundaries, tag-based data sources cause fewer headaches.

Design Scenario: Platform Team Pattern

Platform team manages networking (VPC, subnets, security groups). App team deploys workloads into the platform team’s network.

# App team's compute workspace
data "terraform_remote_state" "platform" {
  backend = "s3"
  config = {
    bucket = "platform-tf-state"   # Platform team's state bucket
    key    = "prod/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

locals {
  vpc_id             = data.terraform_remote_state.platform.outputs.vpc_id
  private_subnet_ids = data.terraform_remote_state.platform.outputs.private_subnet_ids
  app_sg_id          = data.terraform_remote_state.platform.outputs.app_security_group_id
}

resource "aws_ecs_service" "api" {
  ...
  network_configuration {
    subnets         = local.private_subnet_ids
    security_groups = [local.app_sg_id]
  }
}

The app team never needs to know what CIDR ranges the platform team chose. They just consume the outputs. That’s the right level of abstraction.

Key Takeaways

If the source workspace hasn’t been applied yet (no state file exists), terraform_remote_state fails during plan. You have to apply the source workspace first. This creates an implicit ordering that Terraform itself won’t enforce — you need to handle it in your pipeline or runbook.

terraform_remote_state reads state at plan time. If the source workspace is being applied concurrently, you might read stale values. In practice, this means: apply networking first, then compute. Don’t run them in parallel unless you’re okay with eventual consistency.

In HCP Terraform, there’s actually a cleaner mechanism for cross-workspace data sharing — workspace outputs are accessible via the HCP API with proper access controls. Direct S3 access via terraform_remote_state bypasses HCP’s permission model entirely, which is something to think about if you’re paying for HCP’s governance features and then going around them.