Migrating from Native Terraform
This reference is a scenario-keyed decision guide. Identify which shape the user has, then follow the matching recipe. For the full user-facing prose tutorial, see atmos.tools/migration/native-terraform.
A working end-to-end example lives at examples/native-terraform/ in the Atmos repo -- read it
when you need a complete reference for the minimum-viable migration.
Identifying the User's Shape
Before proposing anything, ask the user to show you their repo layout, or read it directly. Most vanilla-Terraform repos fall into one of three shapes:
| Shape | Recipe |
|---|---|
Per-environment dirs (terraform/dev/, terraform/prod/) | Shape A |
One TF dir, env config via -var-file from a Makefile/script | Shape B |
Multiple root modules (terraform/vpc/, terraform/eks/) | Shape C |
Mixed shapes are common -- treat each root module independently.
Shape A: Per-Environment Directories with .tfvars
Before:
terraform/├── vpc/│ ├── main.tf│ ├── variables.tf│ ├── outputs.tf│ └── envs/│ ├── dev.tfvars│ ├── staging.tfvars│ └── prod.tfvars└── database/├── main.tf└── envs/├── dev.tfvars└── prod.tfvars
Recipe:
atmos.yamlat repo root, no file moves:base_path: "./"components:terraform:base_path: "terraform" # Point at the existing dirapply_auto_approve: falsedeploy_run_init: trueauto_generate_backend_file: falsestacks:base_path: "stacks"included_paths: ["**/*"]excluded_paths: ["**/_defaults.yaml"]- Create
stacks/dev.yaml:import:- _defaultscomponents:terraform:vpc:vars: !include ../terraform/vpc/envs/dev.tfvarsdatabase:vars: !include ../terraform/database/envs/dev.tfvars - Run
atmos terraform plan vpc -s dev. Compare to the previouscd terraform/vpc && terraform plan -var-file=envs/dev.tfvarsoutput.
The user keeps their .tfvars files and TF code unchanged. Later, they can convert per-env
.tfvars to native YAML to get deep-merge inheritance across environments.
Shape B: Single Dir with -var-file from a Makefile
Before:
terraform/├── main.tf├── variables.tf└── envs/├── dev.tfvars└── prod.tfvarsMakefile
With a Makefile like terraform plan -var-file=envs/$(ENV).tfvars.
Recipe:
atmos.yaml:base_path: "./"components:terraform:base_path: "." # The whole repo is one componentstacks:base_path: "stacks"included_paths:- "**/*"- The component name must match the physical directory name, unless you set
metadata.component-- Atmos resolves a component to<components.terraform.base_path>/<component_name>, so withbase_path: "."a stack component namedterraformresolves to the real directory with no extra configuration:
If the user wants a friendlier stack component name, such as# stacks/dev.yamlcomponents:terraform:terraform:vars: !include ../terraform/envs/dev.tfvarsinfra, without moving or renaming the directory, setmetadata.componentto the physical directory name instead:# stacks/dev.yamlcomponents:terraform:infra:metadata:component: terraform # points at the existing `terraform/` directoryvars: !include ../terraform/envs/dev.tfvarsmetadata.componentis a pointer to the physical directory, resolved the same way the component name normally is (relative tocomponents.terraform.base_path, or absolute). It is also how you point multiple stack instances at one shared component (see remote-state-bridge.md) -- the no-move rename above is the same mechanism applied to a single-component repo. Only rename the directory on disk (e.g.terraform/toinfra/) if the user prefers a physical rename over an alias. - The Makefile can stay as a thin wrapper around
atmos terraform plan terraform -s devduring transition, then be deleted.
Shape C: Multiple Root Modules with Shared Modules
Before:
terraform/├── vpc/ # root module├── eks/ # root module├── rds/ # root module└── modules/├── label/ # shared module (consumed via source=...)└── tags/ # shared module
Recipe:
- Only root modules become components. Shared modules under
modules/stay where they are and continue to be consumed viasource = "../../modules/foo". Atmos does not care about child modules -- it only orchestrates root modules. - Point
components.terraform.base_path: "terraform". - Define one component per root module (
vpc,eks,rds). - Per-component
.tfvarsget!include'd into stacks per the Shape A recipe.
Do not propose flattening shared modules into components -- that breaks reuse and inverts the purpose of modules.
Common Gotchas
Backend ownership
Atmos generates backend.tf.json at plan/apply time by default. If the user's .tf files
contain a hand-written backend "s3" {} block, the two will conflict at terraform init time.
Two valid paths:
- Recommended: Delete the
backend "s3" {}block from.tffiles and let Atmos own the backend (configured per-stack). This is what enables per-stack backend isolation. - Alternative: Set
auto_generate_backend_file: falseinatmos.yamland keep the hand-written backend block. The user loses per-stack backend flexibility but the migration is zero-risk for code.
TF_VAR_* environment variables
Move Terraform input values into first-class stack vars: when possible:
components:terraform:vpc:vars:foo: bar
Stack-level vars: is preferred because it shows up in atmos describe component and is
deep-merged through inheritance. Do not promote TF_VAR_* under env: as the recommended target
for Terraform input variables.
Use env: for conventional environment variables needed by providers, scripts, or the Terraform
process itself:
components:terraform:vpc:env:AWS_REGION: us-east-1
Existing TF_VAR_* exports can keep working during an initial low-disruption migration, but treat
them as compatibility, not the end-state pattern. Convert them to stack vars: when the value is
a Terraform input variable.
Provider authentication
Provider auth (AWS credentials, Azure subscription, etc.) is orthogonal to migration. The user's existing auth setup (env vars, AWS profiles, instance profiles) keeps working. If they want Atmos to manage identity chains and assume-role flows, route them to the atmos-auth skill -- this is post-migration polish, not a migration prerequisite.
Remote state from un-migrated components
When migrating component-by-component, a new Atmos component will often need to read outputs
from a Terraform root module that hasn't been migrated yet. This is what the
remote-state-bridge.md reference solves -- it lets a real Atmos
component query state from an un-migrated TF dir via !terraform.state without rewriting the
legacy code.
What to NOT Do
- Do not propose moving files into
components/terraform/as step 1. That comes last, after the user has felt the value of Atmos on their existing layout. - Do not rewrite
.tfvarsas YAML on first pass.!includethem. - Do not delete hand-written
backend "s3" {}blocks without asking -- ask first which path the user prefers (Atmos-managed vs hand-written). - Do not introduce Gomplate datasources for things YAML functions can express. See the Core Principles in the SKILL.md.