Migrating from Terramate
Terramate and Atmos solve a similar problem—managing Terraform/OpenTofu at scale with DRY, multi-environment configuration. The biggest conceptual difference is code generation: Terramate typically generates each stack's Terraform module-calling code from templates, while Atmos treats Terraform root modules as static, reusable code and only generates thin boilerplate (backend, provider files) around them. This guide will help you understand the differences and migrate your infrastructure.
Key Differences at a Glance
| Concept | Terramate | Atmos |
|---|---|---|
| Configuration Format | HCL (*.tm.hcl) | YAML (.yaml) |
| Reuse Mechanism | globals {} + generate_hcl | import: with deep merge + static components |
| Module Wiring | generate_hcl commonly assembles the module "x" {} call itself from templated globals | The component (Terraform root module) is static, checked-in code; only vars: change per instance |
| Dependencies | after/before stack ordering, or plain Terraform data lookups between stacks | dependencies.components for ordering and !terraform.output/!terraform.state for values |
| Variable Passing | globals "a" "b" {}, hierarchical by directory | vars: with inheritance via imports |
| Backend/Provider Generation | generate_hcl mixins hand-template backend.tf/provider.tf | Native backend:/providers: sections—no generator to write |
| Tags | tags = [...] (list only) + --tags | metadata.tags (list) and metadata.labels (map) + --tags/--labels |
| Orchestration | script {} blocks + terramate script run | workflows: + custom commands + dependencies.components |
| Change Detection | terramate list --changed | atmos list affected / atmos describe affected |
| Module Source | generate_hcl module block, or a plain source = "..." | source: for JIT provisioning, or vendor.yaml for vendoring |
| Stacks | Directory containing a stack {} block (conventionally in stack.tm.hcl) | Stack file (YAML), not directory-bound |
| Cloud Dashboard | Terramate Cloud | Atmos Pro |
| CLI | terramate script run -- deploy | atmos terraform plan/apply/deploy, atmos workflow <name> |
What Atmos Has That Terramate Doesn't
| Feature | Description |
|---|---|
| Native Authentication | Built-in multi-cloud auth with SAML, SSO, OIDC, and GitHub Actions. No separate tools needed—atmos auth login handles it all. |
| Vendoring | Pull and version external modules locally with atmos vendor pull. Terramate has no dedicated vendoring system—module sources are just Terraform's native source =. |
| Label filtering (AND semantics) | Terramate tags are list-only, OR-matched. Atmos adds metadata.labels (key: value map, matched with --labels on all given pairs)—closer to a Kubernetes label selector. |
| Custom Commands | Define your own CLI verbs in YAML. Terramate's script {} runs a fixed set of job commands per invocation; Atmos custom commands extend the atmos CLI surface itself. |
| Terraform Shell | atmos terraform shell vpc -s prod drops you into a configured shell for native Terraform debugging. All vars and backend pre-configured. |
| Component Validation | JSON Schema and OPA policy validation for stack configurations before deployment. |
| Configuration Provenance | atmos describe component --provenance traces where every value came from across the import hierarchy—useful since Atmos has no directory-position-based origin the way Terramate's globals do. |
| Dependency-Closure Selection | --include-dependencies/--include-dependents expand any selector (--tags, --labels, --affected, -s) through the dependency graph at runtime—broader than Terramate's static after/before ordering. |
Directory Structure Comparison
- Terramate
- Atmos
Terramate stacks are directories. A stack {} block—conventionally defined in a file named stack.tm.hcl, though Terramate detects it regardless of filename—marks the directory as a plannable unit, and generate_hcl blocks in imports/ templates emit the actual .tf files into each stack directory before Terraform runs.
infrastructure/
├── terramate.tm.hcl # Root Terramate config
├── config.tm.hcl # Root globals (backend, providers, versions)
├── imports.tm.hcl # import { source = "./imports/**/*.tm.hcl" }
├── imports/
│ ├── mixins/
│ │ ├── backend.tm.hcl # generate_hcl "backend.tf"
│ │ └── terraform.tm.hcl # generate_hcl "terraform.tf" (providers)
│ └── generators/v1/
│ └── generate_vpc.tm.hcl # generate_hcl "main.tf" (module wiring)
└── stacks/
├── prod/
│ ├── vpc/
│ │ ├── stack.tm.hcl # tags, id, after
│ │ ├── main.tf # GENERATED — do not edit
│ │ ├── backend.tf # GENERATED — do not edit
│ │ └── terraform.tf # GENERATED — do not edit
│ └── eks/
│ └── stack.tm.hcl # after = ["tag:vpc"]
└── dev/
└── vpc/
└── stack.tm.hcl
Characteristics:
- Every stack directory holds a mix of hand-written config (
stack.tm.hcl,config.tm.hcl) and generated Terraform (main.tf,backend.tf,terraform.tf) - The module-calling code is an output artifact—edit the generator template, then
terramate generate, never the.tffile directly - Directory position determines both the stack identity and the
globalsinheritance chain
With Atmos, components (Terraform root modules) and stacks (YAML config) are cleanly separated, and neither is code-generated from templates. A component is checked-in, static Terraform; a stack is a YAML file that instantiates it with vars:.
infrastructure/
├── atmos.yaml # Atmos config (YAML)
├── components/
│ └── terraform/
│ ├── vpc/
│ │ ├── main.tf # Native Terraform, never generated
│ │ └── variables.tf
│ └── eks/
│ ├── main.tf
│ └── variables.tf
└── stacks/
├── _defaults/
│ └── globals.yaml # Shared backend/provider defaults
├── prod/
│ └── us-east-1.yaml # vpc + eks instances, tagged
└── dev/
└── us-east-1.yaml
Characteristics:
- Component code is fixed; only
vars:differ per stack instance—no generator template to keep in sync backend:/providers:are native stack sections, auto-generated by Atmos itself—no hand-writtengenerate_hclmixin required- One component can be instantiated by any number of stacks with no per-stack copy of its Terraform code
Key difference: Terramate typically treats each stack's Terraform code as an output of code generation; Atmos treats it as a fixed, portable artifact and only varies configuration around it. This is why a Terramate migration has an extra step the Terragrunt or Native Terraform migrations don't: decompiling a generate_hcl generator's templated output back into a real, static .tf file once.
Configuration inheritance and merging in Atmos come from YAML stack files and their import: graph, not from directory position. This means:
- You can query any component's fully-resolved configuration with
atmos describe component - You can trace where any value came from with
atmos describe component --provenance - A component's
vars/locals/metadatanever depend on which directory its stack file happens to live in
This differs from Terramate, where a stack's identity, its globals inheritance, and (commonly) its generated Terraform code all derive from where the stack {} block sits in the directory tree.
One place the filesystem still matters in Atmos: when a stack has no explicit name, no name_template, and no name_pattern, Atmos falls back to the stack filename's basename (e.g., prod.yaml → prod) as its logical name—and that name drives -s selection, dependencies.components[].stack, and Terraform workspace naming. Set an explicit name (or a consistent name_template) on migrated stacks rather than relying on this fallback. See Stack Names for the full precedence order.
Concept Mapping
If you're familiar with Terramate, the sections below translate what you already know into Atmos equivalents.
Stack → Stack Manifest + Component Instance
- Terramate
- Atmos
A Terramate stack is a directory containing a stack {} block (conventionally in a file named stack.tm.hcl). The id is a stable UUID—used for Cloud sync and commonly interpolated into a generate_hcl backend template, e.g. key = "terraform/stacks/by-id/${terramate.stack.id}/terraform.tfstate"—so it survives directory renames, but it's only one piece of the full generated backend key, not the key itself. tags categorize the stack; after declares ordering against other tagged stacks.
Atmos stacks are not filesystem-bound. A Terramate stack directory typically becomes one component instance inside a named Atmos stack—not a 1:1 directory mapping. Treating them 1:1 is the most common migration mistake; it produces needlessly fragmented stack files.
To preserve existing state, inspect the actual generate_hcl backend template and set the backend key/workspace_key_prefix to match the full existing key exactly (prefix, UUID, and filename—not just the id)—see Stack Naming for Migrations below.
Globals → Vars
- Terramate
- Atmos
Terramate globals are namespaced (globals "a" "b" {}) and deep-merge hierarchically down the directory tree—root config → environment directory → stack directory.
Atmos vars/locals deep-merge through the explicit import: graph—the same idea, but not tied to directory position. Atmos is a superset here: use !terraform.output/!terraform.state/atmos.Component() for cross-stack reads that Terramate globals can't express natively.
generate_hcl Mixins → Backend / Providers
- Terramate
- Atmos
Near-universal plumbing—backend and provider config—is templated in a generate_hcl mixin and emitted into every stack.
Atmos generates backend.tf.json/providers_override.tf.json natively from stack sections—no generate_hcl block to hand-write or keep in sync.
generate_hcl Generators → Component + Vendoring
- Terramate
- Atmos
Per-layer module wiring—the actual module "vpc" { ... } call—is commonly hand-templated and generated from globals, often gated by a condition for staged template-version rollouts.
This is a mental-model shift, not a syntax swap: the module is the component—static, checked-in code. Only vars: vary per stack.
For modules reused across many stacks, consolidate the source/version pin into one vendor.yaml entry (atmos vendor pull) instead of repeating it per generator—an auditable, diffable manifest in place of scattered version pins.
Terramate's generators/v1/v2 directories, gated by condition, map to two Atmos options: separate component directories (vpc, vpc-v2) selected per-stack for a breaking module rewrite, or a per-stack source/version override on the same component for a plain version bump.
script {} → Workflows, Custom Commands, and Dependencies
- Terramate
- Atmos
script {} bundles job commands, Terramate Cloud sync flags, and (via terramate script run --changed --tags) filtering into one construct.
Atmos splits the same job across three primitives instead of one: dependencies.components for ordering, workflows: for the multi-step job itself, and (optionally) a custom command for a shorter top-level verb.
atmos workflow deploy -s prod-us-east-1
atmos workflow <name> is already the direct equivalent of terramate script run -- <name>—wrapping it in a custom command is optional. Terramate Cloud's sync flags map to distinct Atmos Pro mechanisms: sync_preview/sync_deployment (PR-triggered plan/apply dispatch) → settings.pro.pull_request; sync_drift_status → settings.pro.drift_detection plus --upload-status on the relevant atmos terraform plan step. atmos pro lock/unlock only guard against concurrent operations on a stack—they don't report status. atmos pro commit creates Git commits through the Atmos Pro GitHub App (e.g. committing terraform fmt output)—it isn't a deployment/preview/drift reporting mechanism either.
Tags → metadata.tags / metadata.labels
- Terramate
- Atmos
Tags are a flat list, used for CLI filtering, conditional generation, and dependency ordering.
terramate list --tags kubernetes
terramate script run --tags kubernetes -- deploy
Atmos has both list-form tags (OR-matched by --tags) and map-form labels (AND-matched by --labels), plus stack-wide defaults that deep-merge into every component in a stack.
atmos terraform plan --tags kubernetes,production
atmos terraform apply --labels cost-center=platform
Read a component's own tags/labels at runtime with the !tags/!labels YAML functions—rarely needed the way Terramate needs tm_contains(terramate.stack.tags, ...), since Atmos components don't hand-generate conditional HCL.
The one remaining nuance: Terramate's after = ["tag:vpc"] has no manifest-level equivalent—dependencies.components[] only accepts name/component/stack/kind, not a tag selector, and --include-dependencies/--include-dependents don't derive ordering from tags either—they only expand an already-declared graph. The edge itself must be declared once with an explicit name: entry (eks depends on vpc). After that, tags/labels pick a seed for ad hoc/CI-scoped runs and the closure flags expand it: --tags kubernetes --include-dependencies runs eks plus what it depends on (vpc); --tags vpc --include-dependents runs vpc plus everything that depends on it (eks).
Function Mapping
Terramate exposes tm_* HCL functions, resolved entirely at terramate generate time. Atmos moves the same workflows to Go templates (Sprig/Gomplate) or YAML functions.
| Terramate Function | Atmos Equivalent | Notes |
|---|---|---|
tm_try(a, b, default) | Sprig default, or {{ if }} | Fallback if a value errors/is unset |
tm_contains(list, item) | Sprig has | Membership test |
tm_alltrue(list) | Sprig/template and chain, or logic in locals: | AND-combine boolean conditions |
tm_length(x) / tm_split(sep, x) | Sprig len / splitList | |
tm_can(expr) | No direct equivalent—use error-tolerant {{ if }} guards | |
tm_dynamic | Stays in .tf (Terraform's own dynamic block), or !template/template loops for stack-level generation | Atmos rarely needs this since modules aren't hand-assembled |
Atmos offers two ways to access dynamic values:
- YAML Functions (
!exec,!env,!terraform.output,!tags,!labels) — Preferred. Validated at parse time, readable, works with YAML tooling. - Go Templates (
{{ env "VAR" }}) — Escape hatch when YAML functions don't cover the use case.
See YAML Functions for the complete reference.
CLI Command Comparison
- Terramate
- Atmos
Terramate commands scope to a directory with -C, and script run invokes a named job across all matching stacks.
# Plan/deploy one stack
terramate script run -C stacks/prod/vpc preview
terramate script run -C stacks/prod/vpc deploy
# What changed since the last commit
terramate list --changed
# Deploy everything changed, in dependency order
terramate script run --changed --parallel 4 -- deploy
# Reconcile only drifted, tagged stacks
terramate script run --tags reconcile --status=drifted -- drift reconcile
Atmos commands run from anywhere in the repo—no directory scoping needed.
# Plan/apply one component
atmos terraform plan vpc -s prod-us-east-1
atmos terraform apply vpc -s prod-us-east-1
# What changed since the last commit
atmos list affected # human-readable table/JSON/YAML/CSV/tree
atmos describe affected # deep, machine-readable output for scripting/CI
# Apply everything affected, in dependency order
atmos terraform apply --affected --max-concurrency 4
# Apply only tagged components (composes with --labels, --affected, --all)
atmos terraform apply --tags reconcile
# Expand a tag-selected seed set through the dependency graph
atmos terraform plan --labels env=prod --include-dependencies
Migration Steps
Step 1: Convert stack.tm.hcl + globals to Stack YAML
- Before (Terramate)
- After (Atmos)
Step 2: Decompile Generated Modules into Static Components
In Terraform, a root module is the top-level directory where you run terraform plan/terraform apply; it has its own state. In Atmos terminology, root modules are called components.
- Before (Terramate)
- After (Atmos)
The module call lives only as a generate_hcl template—there's no checked-in main.tf to move.
imports/generators/v1/
└── generate_vpc.tm.hcl # templates module "vpc" { ... }
Run terramate generate once to materialize the current output, then copy the generated main.tf into a component directory and replace the literal global.* values with var.* references.
components/terraform/
└── vpc/
├── main.tf # the decompiled module block, now static
└── variables.tf
This is the one step with no equivalent in a Terragrunt or Native Terraform migration, since those tools never hand-generate the module-calling code in the first place.
Step 3: Replace generate_hcl Mixins with Native Backend/Provider Sections
- Before (Terramate)
- After (Atmos)
Atmos generates backend.tf.json automatically before terraform init—no mixin required. To keep reading/writing the same state Terramate wrote, inspect the actual generate_hcl backend template and set the backend key/workspace_key_prefix to match the full existing key exactly (prefix, UUID, and filename—not just the id).
Step 4: Convert script {} to Workflows
- Before (Terramate)
- After (Atmos)
Unlike the Terramate example, this intentionally re-plans immediately before applying rather than reusing a separately saved out.tfplan—the recommended default, since it applies against the freshest possible plan. To instead apply an exact, previously reviewed plan (matching Terramate's saved-plan semantics), generate one with atmos terraform plan vpc (Atmos saves it automatically) and apply that exact plan with atmos terraform deploy vpc --from-plan.
Migration Checklist
- Install Atmos CLI (Installation Guide)
- Create
atmos.yamlconfiguration - Run
terramate generateto materialize the current output of everygenerate_hclgenerator - Decompile generated module blocks into static components under
components/terraform/ - Convert
stack.tm.hcl+globalsto stack YAML - Replace
generate_hclbackend/provider mixins with nativebackend:/providers:sections - Convert
tags/afterordering tometadata.tags/metadata.labelsanddependencies.components - Convert
script {}blocks toworkflows:(and custom commands, if needed) - Match backend state keys exactly to preserve existing state
- Consolidate reused module versions into
vendor.yaml - Replace Terramate Cloud sync flags with Atmos Pro (
settings.pro) - Test with
atmos terraform plan - Update CI/CD pipelines
- Train team on new commands
Stack Naming for Migrations
Stack names identify stacks—used whenever a command targets one specific stack (atmos terraform plan -s <stack>) or a dependency references another stack. Commands that operate across all stacks, like atmos list stacks or atmos terraform apply --affected, don't require a single -s selector. You still need to define how Atmos determines these names for the commands that do.
You have two options:
Option 1: Use name_template (Recommended for Consistent Patterns)
If you have consistent context variables across all your stacks, configure name_template in atmos.yaml to programmatically compute stack names:
Option 2: Use Explicit name Field (For Inconsistent or Legacy Naming)
If your infrastructure doesn't follow a strict naming convention—common when Terramate stack ids and directory names diverged over time—use the name field to explicitly specify the stack name:
The name field takes precedence over name_template, so you can use both—template for most stacks, explicit names for exceptions. For complete documentation, see Stack Names.
Why Migrate?
Advantages of Atmos
- No code generation to keep in sync — components are static Terraform; only configuration varies per stack
- Native backend/provider generation — no
generate_hclmixin to write and maintain - Both tags and labels — list-form OR filtering plus map-form AND filtering, with stack-wide defaults
- Deep merge semantics — imports aren't tied to directory position
- Multi-tool orchestration — not just Terraform (Helmfile, Packer, Ansible, Helm, Kubernetes)
- Active development — regular releases, responsive community
When to Stay with Terramate
- It's working for you — if your team knows Terramate well and has no pain points, there's no reason to change
- Heavy dynamic generation — if you rely on
generate_hclto synthesize substantially different module code per environment (not just config), Terramate's full HCL templating power may be necessary .tmtriggerschange-detection overrides — Terramate's CLI-managed "ignore this change" records have no Atmos equivalent; if your team relies on them heavily, budget time to redesign arounddependencies.files/dependencies.foldersscoping instead
Get Help
Migrating a large codebase? We're here to help:
- Slack Community - Ask migration questions
- Office Hours - Live support for complex migrations
- GitHub Discussions - Share your migration story
Next Steps
Now that you understand the migration path:
- Learn YAML in Atmos - YAML is more powerful than you might think
- Explore YAML Functions -
!terraform.output,!tags,!labels, and more - Try the Quick Start - Get hands-on with Atmos
- Read Core Concepts - Understand Atmos deeply
- Explore Stack Configuration - Advanced YAML features