Sharing Data Between Terraform Configurations Without Creating a Mess
Remote state data sources are the obvious way to share outputs between configs, and the easiest way to build a brittle dependency web. Here are the safer patterns.
- #terraform
- #remote-state
- #outputs
- #modules
- #architecture
- #coupling
Once you split Terraform into more than one configuration — networking here, the platform there, app teams over there — you immediately need to pass data between them. The app config needs the VPC ID. The platform config needs the cluster endpoint. The classic answer is the terraform_remote_state data source, and it works on day one. By month six it can become a tangle of configs that all reach into each other’s state, where touching the networking layer mysteriously breaks an app deploy two teams away.
The problem isn’t the tool — it’s coupling. Here’s how to share data between configurations in a way that doesn’t slowly cement everything into one fragile blob.
The default move: terraform_remote_state
The mechanism is straightforward. One config publishes outputs, another reads its state:
# In the networking config — publish what consumers need
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
# In the app config — read the networking state
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "company-tf-state"
key = "networking/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.network.outputs.private_subnet_ids[0]
}
This is fine. The danger is what it tempts you to do next.
The trap: reading state directly creates tight coupling
terraform_remote_state reads the entire state of another config. That means the consumer needs read access to the producer’s state bucket — and more subtly, it couples the consumer to the producer’s output contract with no enforcement. Rename vpc_id to main_vpc_id in networking and every downstream config breaks on the next plan, with no warning until it happens.
Worse, teams start treating shared state as a global variable space. Twelve configs read the networking state, three of them reach for outputs that were never meant to be public, and now you can’t refactor networking without a cross-team migration.
Pattern 1: treat outputs as a published, versioned contract
The fix is discipline, not a different tool. Decide deliberately which outputs are public API and document them as such. Keep the public surface small and stable. Internal values stay internal — if it’s not meant for consumers, don’t output it.
A useful convention is a single structured output that consumers read, so the contract is one obvious object:
output "network_contract" {
description = "Stable public interface. Do not break without a migration."
value = {
vpc_id = aws_vpc.main.id
private_subnet_ids = aws_subnet.private[*].id
public_subnet_ids = aws_subnet.public[*].id
}
}
Consumers read outputs.network_contract.vpc_id. When you need to change the shape, you can add fields freely; removing or renaming one is now an obvious, reviewable break.
Pattern 2: decouple through a data store instead of state
You don’t have to read another config’s state at all. A cleaner pattern for cross-team boundaries is to have the producer write its public values to a neutral store — SSM Parameter Store, GCP Secret Manager, Consul — and consumers read from the store, not from state:
# Producer writes the contract to SSM
resource "aws_ssm_parameter" "vpc_id" {
name = "/platform/network/vpc_id"
type = "String"
value = aws_vpc.main.id
}
# Consumer reads SSM — no access to producer's state needed
data "aws_ssm_parameter" "vpc_id" {
name = "/platform/network/vpc_id"
}
This breaks the state-access coupling entirely. The consumer never touches the producer’s backend, only a published parameter. It’s more moving parts, but for boundaries between teams or trust domains, it’s the right trade — you’ve turned an implicit, total dependency into an explicit, minimal one.
Pattern 3: let the provider’s own data sources do the work
Often you don’t need remote state or a parameter store, because the data already lives in the cloud and the provider can query it by tag or name:
data "aws_vpc" "main" {
tags = { Name = "platform-main" }
}
data "aws_subnets" "private" {
filter {
name = "vpc-id"
values = [data.aws_vpc.main.id]
}
tags = { Tier = "private" }
}
If your producer tags resources consistently, consumers can discover infrastructure by tag with zero coupling to the producer’s Terraform at all. This is my default when it’s available — the cloud is already the source of truth, so query it directly. The cost is that you depend on a tagging convention being honored, which is itself a contract worth documenting.
Choosing between them
A rough hierarchy I follow:
- Same team, tightly related configs?
terraform_remote_statewith a small, documented contract output is fine and simplest. - Cross-team or cross-trust-boundary? Publish to a parameter store; don’t share state access.
- Data already discoverable in the cloud by tag/name? Use the provider’s data sources and skip the coupling entirely.
The anti-pattern to avoid in all cases: a config that reads five other configs’ raw state and depends on outputs nobody declared as stable. That’s the architecture that makes people afraid to refactor.
Keep the dependency graph legible
Whatever you pick, write down the dependency direction. Producers should not know about consumers; data flows one way. The moment two configs read each other’s state, you have a cycle that no amount of clever HCL untangles cleanly.
Cross-config wiring is also exactly where a refactor goes wrong silently — a renamed output, a removed tag, a flipped dependency direction — and it passes local plan because the break is in a different config. Running these changes through a code review workflow helps catch contract breaks before they cascade. For more on structuring Terraform across teams, see our other Terraform guides.
Data-sharing patterns have different security and operational trade-offs. Verify access controls and provider behavior against your own environment before adopting them.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.