Skip to main content

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

ConceptTerramateAtmos
Configuration FormatHCL (*.tm.hcl)YAML (.yaml)
Reuse Mechanismglobals {} + generate_hclimport: with deep merge + static components
Module Wiringgenerate_hcl commonly assembles the module "x" {} call itself from templated globalsThe component (Terraform root module) is static, checked-in code; only vars: change per instance
Dependenciesafter/before stack ordering, or plain Terraform data lookups between stacksdependencies.components for ordering and !terraform.output/!terraform.state for values
Variable Passingglobals "a" "b" {}, hierarchical by directoryvars: with inheritance via imports
Backend/Provider Generationgenerate_hcl mixins hand-template backend.tf/provider.tfNative backend:/providers: sections—no generator to write
Tagstags = [...] (list only) + --tagsmetadata.tags (list) and metadata.labels (map) + --tags/--labels
Orchestrationscript {} blocks + terramate script runworkflows: + custom commands + dependencies.components
Change Detectionterramate list --changedatmos list affected / atmos describe affected
Module Sourcegenerate_hcl module block, or a plain source = "..."source: for JIT provisioning, or vendor.yaml for vendoring
StacksDirectory containing a stack {} block (conventionally in stack.tm.hcl)Stack file (YAML), not directory-bound
Cloud DashboardTerramate CloudAtmos Pro
CLIterramate script run -- deployatmos terraform plan/apply/deploy, atmos workflow <name>

What Atmos Has That Terramate Doesn't

FeatureDescription
Native AuthenticationBuilt-in multi-cloud auth with SAML, SSO, OIDC, and GitHub Actions. No separate tools needed—atmos auth login handles it all.
VendoringPull 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 CommandsDefine 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 Shellatmos terraform shell vpc -s prod drops you into a configured shell for native Terraform debugging. All vars and backend pre-configured.
Component ValidationJSON Schema and OPA policy validation for stack configurations before deployment.
Configuration Provenanceatmos 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 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 .tf file directly
  • Directory position determines both the stack identity and the globals inheritance chain

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 Lives in YAML, Not the Filesystem

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/metadata never 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.yamlprod) 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

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.

stacks/prod/vpc/stack.tm.hcl
stack {
name = "vpc-prod"
description = "Production VPC"
id = "db0aac90-33e0-48e2-a5b1-5b680b8b2749"
tags = ["vpc", "production"]
}

Globals → Vars

Terramate globals are namespaced (globals "a" "b" {}) and deep-merge hierarchically down the directory tree—root config → environment directory → stack directory.

stacks/prod/config.tm.hcl
globals "vpc" {
vpc_name = "vpc-${global.terraform.env}"
cidr = "10.0.0.0/16"
}

generate_hcl Mixins → Backend / Providers

Near-universal plumbing—backend and provider config—is templated in a generate_hcl mixin and emitted into every stack.

imports/mixins/backend.tm.hcl
generate_hcl "backend.tf" {
content {
terraform {
backend "s3" {
bucket = global.terraform.backend.bucket
key = "terraform/stacks/by-id/${terramate.stack.id}/terraform.tfstate"
region = global.terraform.backend.region
}
}
}
}

generate_hcl Generators → Component + Vendoring

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.

imports/generators/v1/generate_vpc.tm.hcl
generate_hcl "main.tf" {
condition = global.generators.version == "v1"
content {
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.19.0"
name = global.vpc.vpc_name
cidr = global.vpc.cidr
}
}
}

script {} → Workflows, Custom Commands, and Dependencies

script {} bundles job commands, Terramate Cloud sync flags, and (via terramate script run --changed --tags) filtering into one construct.

stacks/workflows.tm.hcl
script "deploy" {
job {
commands = [
["terraform", "validate"],
["terraform", "plan", "-out", "out.tfplan"],
["terraform", "apply", "-auto-approve", "out.tfplan", {
sync_deployment = true
}],
]
}
}

Tags → metadata.tags / metadata.labels

Tags are a flat list, used for CLI filtering, conditional generation, and dependency ordering.

stack.tm.hcl
stack {
tags = ["kubernetes", "production"]
after = ["tag:vpc"]
}
terramate list --tags kubernetes
terramate script run --tags kubernetes -- deploy

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 FunctionAtmos EquivalentNotes
tm_try(a, b, default)Sprig default, or {{ if }}Fallback if a value errors/is unset
tm_contains(list, item)Sprig hasMembership 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_dynamicStays in .tf (Terraform's own dynamic block), or !template/template loops for stack-level generationAtmos rarely needs this since modules aren't hand-assembled
YAML Functions vs Templates

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 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

Migration Steps

Step 1: Convert stack.tm.hcl + globals to Stack YAML

stacks/prod/vpc/stack.tm.hcl
stack {
name = "vpc-prod"
id = "db0aac90-33e0-48e2-a5b1-5b680b8b2749"
tags = ["vpc"]
}
stacks/prod/config.tm.hcl
globals "vpc" {
cidr = "10.0.0.0/16"
}

Step 2: Decompile Generated Modules into Static Components

What's a Root Module?

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.

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" { ... }

Step 3: Replace generate_hcl Mixins with Native Backend/Provider Sections

imports/mixins/backend.tm.hcl
generate_hcl "backend.tf" {
content {
terraform {
backend "s3" {
bucket = global.terraform.backend.bucket
key = "terraform/stacks/by-id/${terramate.stack.id}/terraform.tfstate"
region = global.terraform.backend.region
}
}
}
}

Step 4: Convert script {} to Workflows

stacks/workflows.tm.hcl
script "deploy" {
job {
commands = [
["terraform", "plan", "-out", "out.tfplan"],
["terraform", "apply", "-auto-approve", "out.tfplan"],
]
}
}

Migration Checklist

  • Install Atmos CLI (Installation Guide)
  • Create atmos.yaml configuration
  • Run terramate generate to materialize the current output of every generate_hcl generator
  • Decompile generated module blocks into static components under components/terraform/
  • Convert stack.tm.hcl + globals to stack YAML
  • Replace generate_hcl backend/provider mixins with native backend:/providers: sections
  • Convert tags/after ordering to metadata.tags/metadata.labels and dependencies.components
  • Convert script {} blocks to workflows: (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:

If you have consistent context variables across all your stacks, configure name_template in atmos.yaml to programmatically compute stack names:

atmos.yaml
stacks:
name_template: "{{ .vars.environment }}-{{ .vars.stage }}"

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:

stacks/us-east-1/prod/vpc.yaml
name: "prod-us-east-1-vpc"

components:
terraform:
vpc:
vars:
cidr: "10.0.0.0/16"

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_hcl mixin 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_hcl to synthesize substantially different module code per environment (not just config), Terramate's full HCL templating power may be necessary
  • .tmtriggers change-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 around dependencies.files/dependencies.folders scoping instead

Get Help

Migrating a large codebase? We're here to help:

Next Steps

Now that you understand the migration path: