This reference covers a pattern that solves two related but distinct problems with one mechanism. Understand both use cases before applying it -- a single set of YAML primitives serves both transient migration and permanent multi-repo architectures.
The pattern registers an external Terraform root module as a queryable component in your
Atmos stacks, regardless of whether you own the actual Terraform code. Once registered, a real
Atmos component can pull outputs from it using !terraform.state just like any other Atmos
component.
This works whether the "external" root module is:
- A legacy vanilla-Terraform directory in the same repo that hasn't been migrated yet
- A root module owned by another repository (e.g., platform team's networking repo)
- A root module owned by another AWS account / GCP project / Azure subscription
The mechanism in all cases is the same: define a stack instance with metadata.component
pointing at a placeholder (or the real component as abstract), and use per-instance backend
overrides to point at the external state file. The deploy guard (metadata.enabled: false or
metadata.type: abstract) ensures CI/CD doesn't try to plan or apply against it.
The user is migrating a multi-module Terraform repo to Atmos one component at a time. Some components are now Atmos components; others are still vanilla TF directories. A new Atmos component needs to read outputs from one of the un-migrated dirs.
- Lifetime: Temporary. Bridge entries get deleted as each legacy directory becomes a real Atmos component.
- Best variant: Variant A -- a single tiny
dummycomponent serves as the placeholder for all legacy state files.
A workload repository needs to read outputs from components owned by a separate platform, networking, or security repository. The dependency is permanent -- the workload repo declares "these external components exist, here's where their state lives, and here's how I want to reference them."
- Lifetime: Permanent. The bridge is the workload repo's contract with the platform repo.
- Best variant: Variant B -- reference the real
component (vendored or via shared catalog) as
type: abstract, with a cross-account read-only role for state access.
Both use cases share the same primitives. The variants differ in whether you need the real component's code present at all.
Use when the component does not yet exist in Atmos at all and you just need to expose its state
for !terraform.state lookups.
Create components/terraform/dummy/ with a single minimal .tf file. Atmos only needs a
directory it recognizes as a component -- no resources are ever provisioned because the dummy
is never deployed.
# components/terraform/dummy/main.tf
# Placeholder component used by the remote-state bridge pattern.
# This component is never deployed; it exists only so Atmos can resolve
# stack instances that override `metadata.component: dummy` and use
# backend overrides to point at external Terraform state files.
terraform {
required_version = ">= 1.0"
}If the user follows Cloud Posse's context.tf convention, they can additionally drop in
context.tf from terraform-null-label -- it adds nothing functional to a dummy but keeps the
component file structure uniform with their other components.
For each un-migrated Terraform root module whose state you need to read, add an instance to the appropriate stack:
components:
terraform:
vpc/us-east-1-development:
metadata:
enabled: false # Blocks CI/CD plan/apply -- critical
terraform_workspace: "default" # Match the legacy workspace name exactly
component: dummy # Point at the stub component
name: vpc/us-east-1-development # Stable instance name for !terraform.state lookups
backend:
s3:
key: infra/terraform/123456789012/vpc/us-east-1-development/terraform.tfstateThe instance name (the YAML key, vpc/us-east-1-development) is what you'll use in
!terraform.state calls. Pick a name that matches how the user thinks about the legacy module.
From any real Atmos component in the same stack:
components:
terraform:
eks-cluster:
vars:
vpc_id: !terraform.state vpc/us-east-1-development vpc_id
subnet_ids: !terraform.state vpc/us-east-1-development private_subnet_idsAtmos resolves these by reading the legacy state file directly via the backend config in the dummy instance. The legacy Terraform code does not need to know Atmos exists.
Without it, atmos describe affected and CI/CD pipelines will treat the dummy instance as a
deployable component. A pipeline could attempt to terraform plan against it, and since the
dummy has no resources, the plan would propose destroying everything in the legacy state file.
Always set enabled: false on Variant A instances.
Once a legacy directory becomes a real Atmos component, delete the corresponding bridge
instance. The !terraform.state lookups can stay -- they'll resolve against the new real
component automatically (the instance name is what matters, and you can preserve it via the
new component's metadata.name).
Use when the real component code exists (perhaps vendored, perhaps in another repo / account) and you want to declare a permanent state-reading dependency on it.
components:
terraform:
ecs/cluster:
metadata:
component: ecs # The real component
type: abstract # Blocks direct deploy
terraform_workspace: '{{ .vars.tenant }}-{{ .vars.environment }}-{{ .vars.deps_stage }}-{{ .atmos_component | regexp.ReplaceLiteral "\\W" "-" }}'
backend_type: s3
backend:
s3:
bucket: platform-team-tfstate # Platform team's state bucket
encrypt: true
key: terraform.tfstate
acl: bucket-owner-full-control
region: us-east-2
assume_role:
role_arn: arn:aws:iam::123456789012:role/TerraformStateReadOnly # READ-ONLY rolemetadata.type: abstract declares this instance is not directly deployable but is intended as a
base for inheritance or external reference. Other Atmos commands (like atmos list components)
distinguish abstract components from concrete ones. For the bridge pattern, either guard works
-- pick the one that matches the user's mental model:
type: abstract-- "this is a definition, not a deployable" (best when the real component exists but is owned elsewhere)enabled: false-- "this is disabled for this stack" (best when the component is a true placeholder)
The upstream owner (platform team) writes state with their own workspace naming convention.
Your bridge instance must match that convention exactly to find the right state file.
Templating it from vars.tenant, vars.environment, etc. keeps the bridge in sync as the
upstream evolves -- you change vars in one place, not workspace names across many files.
The bridge only needs to read state. Use a dedicated read-only role
(naming convention: *-ro) for the assume_role.role_arn. This:
- Prevents the workload repo from accidentally writing to platform-owned state
- Satisfies least-privilege requirements for security review
- Makes audit logs unambiguous about which side wrote the state
Have the platform team publish the read-only role ARN as part of their consumer interface documentation.
| Situation | Use |
|---|---|
| Legacy TF dir, no Atmos component for it yet, default workspace | Variant A (dummy + enabled: false) |
| Legacy TF dir, non-default workspace convention | Variant A with terraform_workspace set to match |
| Real component owned by another team / repo / account | Variant B (real component + type: abstract) |
| State lives in a different cloud account/project | Variant B with assume_role to a read-only role |
| You'll delete the bridge after migration completes | Variant A -- easier to remove cleanly |
| Permanent multi-repo workload→platform contract | Variant B -- documents the dependency in code |
- Forgetting the deploy guard. Without
enabled: falseortype: abstract, CI will try to plan/apply against the bridge instance. For Variant A this can propose destroying everything in the legacy state. - Wrong workspace name.
terraform_workspacemust match the actual workspace the state was written under. A typo silently creates a new (empty) workspace and!terraform.statereturns nothing useful. - Using a read/write role for cross-account state access. The bridge only reads. Use a
*-rorole and have the state owner create it. - Bridging your own active state with the same component twice. If you have a real Atmos component AND a bridge instance both pointing at the same backend key, the real component's plan/apply will fight the bridge. Bridges are for state you don't own (or haven't migrated yet); never bridge a component you're actively deploying.
- Forgetting to delete migrated bridges. Variant A entries should be deleted as their legacy backing directory becomes a real Atmos component. Stale bridges add noise and can mask schema mismatches.
- atmos-yaml-functions -- for
!terraform.statesyntax and YQ expressions - atmos-stacks -- for
metadata.component,metadata.name, inheritance semantics - atmos-components -- for
type: abstractsemantics and concrete-vs-abstract patterns - atmos-auth -- for cross-account role configuration patterns