Skip to main content

Migrating to Next-Gen Components with Atmos Auth

Ben Smith
Cloud Posse

Over the past couple of months, we've shipped two changes that fundamentally simplify how components authenticate with AWS: Atmos Auth and the deprecation of account-map. This post is a migration guide for updating your component providers to match the version of infrastructure you're running — whether you're moving to next-gen or staying on legacy.

Which Guide Do You Need?

Your situationGuide
You're on legacy infrastructure (account-map, team roles) and want to move to Atmos AuthMigration Guide 1: Upgrading to Next-Gen
You're on legacy infrastructure but want to use a newer component version that ships with next-gen providersMigration Guide 2: New Components on Legacy Infrastructure

Not sure which you are? Read the How to Tell Which Generation You're On section first.

Why We Made These Changes

The driving force behind these changes is testability. The legacy account-map component was a globally required dependency — every component's providers.tf referenced it to resolve IAM roles. That tight coupling meant you couldn't test a single component in isolation without first deploying account-map and its entire dependency chain. By removing account-map from the default providers.tf, components become self-contained and significantly simpler to test.

Atmos Auth reinforces this by moving authentication out of the Terraform layer entirely. Instead of chained role assumptions wired through provider configuration, Atmos Auth resolves credentials before Terraform ever runs. The result is a provider block with nothing but region = var.region — no dynamic lookups, no remote state, no implicit dependencies on other components.

The trade-off is that this is a breaking change. Components that ship with the new providers.tf are not backwards compatible with environments still relying on account-map and team roles. That's exactly why we provide provider override mechanisms (described below) — they let you adopt the new authentication model at your own pace, on any component version, without waiting for upstream releases.

All Cloud Posse Components Are Still Usable

Every component we publish remains fully usable during this transition. The key thing to understand is that providers.tf is what dictates which generation you're on — not the component version itself. As we upgrade components to ship with the next-gen providers.tf, you'll want to check the providers.tf of each component when you vendor a new version. If the upstream providers.tf has changed to the next-gen format and your infrastructure isn't ready for that yet, you can always override it — either via atmos generate in your stack config or by vendoring in a providers.tf mixin — to match your current setup.

What Changed

Two major improvements landed recently:

ChangeWhat It Does
Atmos AuthHandles AWS authentication before Terraform runs — no more dynamic role assumption in providers.tf
Account-Map DeprecationReplaces the account-map Terraform component with a static YAML variable, eliminating a critical deploy-time dependency

Together, these remove the need for account-map, aws-teams, and aws-team-roles. If you missed the announcement, see Reference Architecture v2: Deprecating Account-Map.

What "Generation" Means

The generation is determined entirely by how providers.tf authenticates — not by component version numbers, module logic, or Terraform state format. A component's business logic (resources, variables, outputs) is unaffected by this change. The only thing that changes is the provider configuration and how credentials are resolved.

  1. Legacyproviders.tf uses module.iam_roles to dynamically look up and assume an IAM role via account-map remote state
  2. Next-Genproviders.tf uses region = var.region with no role assumption; Atmos Auth has already set the correct credentials before Terraform runs

How to Tell Which Generation You're On

The quickest way to tell is to look at your component's providers.tf. The majority of components use the module.iam_roles pattern described below. However, some cold start components (like account, account-map, account-settings, tfstate-backend) have always used a simple provider "aws" { region = var.region } — that's by design, because they run under your super admin profile before IAM roles infrastructure exists. Don't confuse a cold start component's simple provider with the next-gen pattern.

Legacy (Account-Map + Team Roles)

If your providers.tf looks like this, you're on the older generation:

provider "aws" {
region = var.region

# Profile is deprecated in favor of terraform_role_arn. When profiles are not in use, terraform_profile_name is null.
profile = module.iam_roles.terraform_profile_name

dynamic "assume_role" {
# module.iam_roles.terraform_role_arn may be null, in which case do not assume a role.
for_each = compact([module.iam_roles.terraform_role_arn])
content {
role_arn = assume_role.value
}
}
}

module "iam_roles" {
source = "../account-map/modules/iam-roles"
context = module.this.context
}

This pattern depends on account-map, aws-teams, and aws-team-roles being deployed. The iam_roles module reaches into the account-map remote state to figure out which role to assume.

Next-Gen (Atmos Auth)

With Atmos Auth, authentication happens before Terraform runs — no dynamic role assumptions, no remote state lookups, no account-map dependency. The next-gen providers.tf includes the account_map variable (a static map of account names to IDs) and a dummy iam_roles module for backward compatibility:

variable "account_map_enabled" {
type = bool
description = "Enable the account map component"
default = false
}

variable "account_map" {
type = object({
full_account_map = map(string)
audit_account_account_name = optional(string, "")
root_account_account_name = optional(string, "")
identity_account_account_name = optional(string, "")
aws_partition = optional(string, "aws")
iam_role_arn_templates = optional(map(string), {})
})
description = "Map of account names to account IDs."
default = {
full_account_map = {}
audit_account_account_name = ""
root_account_account_name = ""
identity_account_account_name = ""
aws_partition = "aws"
iam_role_arn_templates = {}
}
}

provider "aws" {
region = var.region
}

# Dummy module to satisfy legacy references to module.iam_roles
module "iam_roles" {
source = "cloudposse/label/null"
context = module.this.context
}

Why the Dummy iam_roles Module?

Many components reference module.iam_roles in their code — for example, to pass module.iam_roles.terraform_role_arn to sub-providers. The dummy module (sourced from cloudposse/label/null) satisfies Terraform's module reference validation so these components don't error during terraform init. It outputs empty/null values, which means any dynamic "assume_role" blocks that iterate over compact([module.iam_roles.terraform_role_arn]) simply produce zero iterations — no role is assumed, and Atmos Auth's pre-configured credentials are used instead.

Atmos Auth Prerequisites

Before using Atmos Auth, ensure you have:

  1. Atmos >= v1.155.0 — Atmos Auth requires a recent version of the Atmos CLI. Run atmos version to check.
  2. AWS IAM Identity Center (SSO) — For human users, Atmos Auth profiles authenticate via AWS SSO. You need IAM Identity Center configured in your core-root account with Permission Sets for Terraform access.
  3. IAM roles for CI/CD — For machine users, deploy the iam-role component with GitHub OIDC (or your CI provider's equivalent) in each target account.
  4. Atmos Auth profiles configured — Define profiles in your atmos.yaml that map to SSO Permission Sets or IAM roles. See Atmos Auth for the configuration reference.

How Provider Overrides Work

Both migration guides below rely on overriding a component's providers.tf so it matches your infrastructure. There are two mechanisms for this:

The terraform.generate block in your stack configuration tells Atmos to write files into the component directory before Terraform runs. Define it once at any stack level (org, tenant, stage) and every component that inherits from that config gets the generated providers.tf automatically. This is the recommended approach because it's centrally managed, inheritable, and doesn't require per-component changes.

Alternative: Vendor Mixins

We also publish Atmos vendor mixins that can be added to individual component.yaml files. This is useful when you need per-component control or can't use atmos generate for a specific component. The two relevant mixins are:

provider-without-account-map.tf — The next-gen provider. Defines the account_map variables, configures provider "aws" with just region = var.region, and includes a dummy iam_roles module for backward compatibility.

provider-with-account-map.tf — The legacy provider. Restores the module.iam_roles pattern for components that need to run on infrastructure still using account-map.

Account Verification

Regardless of which override method you use, we recommend adding the account-verification.mixin.tf mixin. This uses an aws_caller_identity data source to check the account ID of the credentials Terraform is running with, then compares it against the expected account from the account_map variable. The check runs during terraform plan — if there's a mismatch, Terraform fails before making any changes. This catches misconfigured Atmos Auth profiles, stale SSO sessions, or wrong environment targets.

Example failure output:

Error: Account verification failed
Expected account "567890123456" (plat-dev) but authenticated to "789012345678" (plat-prod).
Check your Atmos Auth profile configuration.

Migration Guide 1: Upgrading Your Infrastructure to Next-Gen

Scenario: You're running legacy infrastructure with account-map, aws-teams, and aws-team-roles. You want to adopt Atmos Auth and remove the account-map dependency.

This is the full infrastructure migration — you're changing how your platform authenticates.

1. Set Up Atmos Auth

Configure Atmos Auth profiles in your atmos.yaml. This tells Atmos how to authenticate to each account before Terraform runs:

# Authenticate with your profile
atmos auth login

See Atmos Auth for configuration details and the prerequisites section above.

2. Add the Static Account Map

Define account IDs in your stack defaults so components can look up accounts without remote state:

# stacks/orgs/acme/_defaults.yaml
vars:
account_map_enabled: false
account_map:
full_account_map:
core-root: "123456789012"
core-artifacts: "234567890123"
core-audit: "345678901234"
core-auto: "456789012345"
plat-dev: "567890123456"
plat-staging: "678901234567"
plat-prod: "789012345678"
root_account_account_name: core-root
audit_account_account_name: core-audit
iam_role_arn_templates:
core-root: "arn:aws:iam::123456789012:role/acme-core-gbl-root-%s"
core-artifacts: "arn:aws:iam::234567890123:role/acme-core-gbl-artifacts-%s"
core-audit: "arn:aws:iam::345678901234:role/acme-core-gbl-audit-%s"
core-auto: "arn:aws:iam::456789012345:role/acme-core-gbl-auto-%s"
plat-dev: "arn:aws:iam::567890123456:role/acme-plat-gbl-dev-%s"
plat-staging: "arn:aws:iam::678901234567:role/acme-plat-gbl-staging-%s"
plat-prod: "arn:aws:iam::789012345678:role/acme-plat-gbl-prod-%s"

The iam_role_arn_templates map provides ARN templates for each account. The %s placeholder is replaced with the role name (e.g., terraform, planner) at runtime by components that need to assume cross-account roles.

3. Override providers.tf for Your Components

Add the next-gen provider configuration to your stack defaults using atmos generate. This generates providers.tf for all components that inherit from this config:

# stacks/orgs/acme/_defaults.yaml (or any inherited stack config)
terraform:
generate:
"providers.tf": |
variable "account_map_enabled" {
type = bool
description = "Enable the account map component"
default = false
}

variable "account_map" {
type = object({
full_account_map = map(string)
audit_account_account_name = optional(string, "")
root_account_account_name = optional(string, "")
identity_account_account_name = optional(string, "")
aws_partition = optional(string, "aws")
iam_role_arn_templates = optional(map(string), {})
})
description = "Static account map for components when account_map_enabled is false."
default = {
full_account_map = {}
audit_account_account_name = ""
root_account_account_name = ""
identity_account_account_name = ""
aws_partition = "aws"
iam_role_arn_templates = {}
}
}

provider "aws" {
region = var.region
}

# Stub module that satisfies references to module.iam_roles in
# upstream components. With Atmos Auth this is no longer needed,
# so we replace it with a no-op label module.
module "iam_roles" {
source = "cloudposse/label/null"
context = module.this.context
}

# TEMPORARY: Override file to declare stale providers so OpenTofu can
# load existing state that still references module.iam_roles from
# account-map/modules/iam-roles. Override files merge with existing
# required_providers blocks instead of conflicting.
# Remove this after all components have been migrated and state is clean.
"versions_override.tf": |
terraform {
required_providers {
awsutils = {
source = "cloudposse/awsutils"
version = ">= 0.1.0"
}
utils = {
source = "cloudposse/utils"
version = ">= 0.1.0"
}
}
}

Components that vendor a providers.tf from upstream need to exclude it so it doesn't conflict with the generated file:

# components/terraform/<component-name>/component.yaml
spec:
source:
excluded_paths:
- "providers.tf"

Alternative: Use Vendor Mixins

If you need per-component control or can't use atmos generate, you can vendor in the provider override directly in each component.yaml:

# components/terraform/<component-name>/component.yaml
apiVersion: atmos/v1
kind: ComponentVendorConfig
spec:
source:
uri: github.com/cloudposse-terraform-components/aws-<component>.git//src?ref={{ .Version }}
version: v1.x.x
included_paths:
- "**/**"
excluded_paths:
- "providers.tf"
mixins:
- uri: https://raw.githubusercontent.com/cloudposse-terraform-components/mixins/{{ .Version }}/src/mixins/provider-without-account-map.tf
version: v0.3.2
filename: providers.tf
- uri: https://raw.githubusercontent.com/cloudposse-terraform-components/mixins/{{ .Version }}/src/mixins/account-verification.mixin.tf
version: v0.3.2
filename: account-verification.mixin.tf

Then re-vendor:

atmos vendor pull -c <component-name>

4. Verify

Run a plan against a non-production environment:

atmos terraform plan <component-name> -s plat-ue1-dev

Confirm the following:

  1. No account-map remote state reads — The plan output should not show any data source reads for account-map state
  2. No assume_role in provider config — Run grep -r "assume_role" components/terraform/<component-name>/providers.tf and confirm no matches
  3. Account verification passes — If you have the account-verification mixin, the plan should complete without an account mismatch error
  4. No unexpected drift — The plan should show no changes (or only expected changes) since only the provider authentication path changed, not the resources themselves

5. Migrate Incrementally

You don't have to do every component at once. Migrate one at a time, verify with a plan, and move on. Components using the next-gen providers.tf and components still on the legacy providers.tf can coexist in the same infrastructure — they just authenticate differently.

For the full migration path including IAM Identity Center setup and removing legacy components, see Migrate from Account-Map.


Migration Guide 2: Using New Component Versions on Legacy Infrastructure

Scenario: You're still running account-map and team roles. You haven't set up Atmos Auth yet. But you want to upgrade to a newer version of a Cloud Posse component, and its providers.tf has already been updated to assume Atmos Auth.

This is the opposite problem — the component has moved to next-gen, but your infrastructure hasn't. The fix is the same tool: vendor in a providers.tf override via component.yaml.

How to Tell If a Component Has Moved to Next-Gen

When you vendor a new version of a component, check its providers.tf. If you see this:

provider "aws" {
region = var.region
}

Instead of the legacy module.iam_roles pattern, the component has been updated to assume Atmos Auth. It won't work out of the box with your account-map-based infrastructure.

The Fix: Vendor in a Legacy-Compatible providers.tf

Override the component's providers.tf in your component.yaml to restore the legacy provider pattern:

# components/terraform/<component-name>/component.yaml
apiVersion: atmos/v1
kind: ComponentVendorConfig
spec:
source:
uri: github.com/cloudposse-terraform-components/aws-<component>.git//src?ref={{ .Version }}
version: v2.x.x # The new version with next-gen providers
included_paths:
- "**/**"
excluded_paths:
- "providers.tf" # Exclude the next-gen providers.tf
mixins:
# Vendor in the legacy providers.tf that works with account-map
- uri: https://raw.githubusercontent.com/cloudposse-terraform-components/mixins/{{ .Version }}/src/mixins/provider-with-account-map.tf
version: v0.3.2
filename: providers.tf

Then re-vendor:

atmos vendor pull -c <component-name>

This gives you the new component code with its bug fixes and features, but keeps the providers.tf compatible with your existing account-map infrastructure.

When to Use This Approach

  1. You need a bug fix or feature from a newer component version
  2. The newer version ships with a next-gen providers.tf
  3. You're not ready to migrate your infrastructure to Atmos Auth yet

This is a bridge strategy — it lets you upgrade components now and migrate your infrastructure later, on your own timeline.

Keep Track of Overrides

When you override providers.tf this way, remember that you've pinned the provider behavior. Once you do migrate your infrastructure to Atmos Auth, come back and switch the mixin to provider-without-account-map.tf (or remove the override entirely if the upstream component already ships with the next-gen version).


What Breaks and Common Errors

Next-Gen Providers on Legacy Infrastructure

If you vendor a component with the next-gen providers.tf but your infrastructure still uses account-map and team roles, Terraform will authenticate with whatever credentials are in your environment (or none at all) instead of assuming the correct role. Common symptoms:

Error: error configuring Terraform AWS Provider: no valid credential sources found
Error: AccessDenied: User: arn:aws:iam::123456789012:user/deploy is not authorized to perform: ...

Fix: Use Migration Guide 2 to vendor in the legacy-compatible providers.tf.

Legacy Providers on Next-Gen Infrastructure

If you still have the legacy providers.tf with module.iam_roles sourced from ../account-map/modules/iam-roles, but you've already removed the account-map component:

Error: Module not found: module.iam_roles
The module at "../account-map/modules/iam-roles" could not be found.

Fix: Use Migration Guide 1 to vendor in the next-gen providers.tf.

Account Verification Mismatch

If Atmos Auth is configured but pointing to the wrong account:

Error: Account verification failed
Expected account "567890123456" (plat-dev) but authenticated to "789012345678" (plat-prod).

Fix: Check your Atmos Auth profile mapping in atmos.yaml and run atmos auth login to refresh credentials.

Terraform State Impact

Switching providers.tf does not require a state migration. The provider configuration change only affects how Terraform authenticates — it doesn't change resource addresses, module paths, or state structure. When you run terraform plan after the migration, you should see no drift related to the provider change itself.

The module.iam_roles is replaced by a dummy module, but since iam_roles only produces outputs consumed within providers.tf (not resources in state), there are no state entries to migrate or remove.

If you do see unexpected drift, it's likely caused by a different component version (new resource defaults, renamed attributes) rather than the provider change. Roll back the component version to isolate whether the drift comes from the provider switch or the component upgrade.

CI/CD Implications

Atmos Auth changes how your CI/CD pipelines authenticate. The specifics depend on your runner.

GitHub Actions

GitHub Actions workflows use OIDC to assume IAM roles directly. Deploy the iam-role component with GitHub OIDC trust policies in each target account. Your workflow authenticates via the standard aws-actions/configure-aws-credentials action before invoking atmos terraform.

Spacelift

Spacelift manages its own AWS credentials via cloud integrations. If you're using Spacelift, it already authenticates before Terraform runs — the next-gen providers.tf (with just region = var.region) aligns naturally with this model. Ensure your Spacelift stacks have the correct AWS integration attached.

Atlantis

Atlantis authenticates via IAM roles configured on the server or via OIDC. Configure your Atlantis server's assumed role to target each account, then use the next-gen providers.tf as-is.

General Pattern

Regardless of runner, the pattern is the same: authenticate before Terraform runs, not inside providers.tf. If your CI system already handles AWS credential setup before executing Terraform commands, it's compatible with next-gen providers.

Summary

BeforeAfter
account-map component deployed firstStatic account_map variable in stack config
aws-teams + aws-team-roles for IAMAWS SSO Permission Sets + Atmos Auth
Dynamic role assumption in providers.tfAtmos Auth handles credentials before Terraform
Complex iam_roles module in every componentSimple region = var.region provider
Tight coupling between components via remote stateComponents are independent

The net result: fewer components to manage, simpler authentication, no deploy ordering dependencies, and a provider configuration you can actually read at a glance.

Learn More

  1. Deprecating Account-Map — The full deprecation announcement
  2. Migrate from Account-Map — Detailed step-by-step migration guide
  3. Atmos Auth — Authentication configuration reference
  4. How to Log into AWS — Authentication workflows for human users
Need Help?

Migrating core authentication infrastructure is a significant change. If you need assistance, reach out in the SweetOps Slack or contact Cloud Posse support.