Skip to main content

File-Scoped Locals: Simplify Stack Configuration with Temporary Variables

· 5 min read
Erik Osterman
Founder @ Cloud Posse

We're introducing file-scoped locals to Atmos stack configurations. Inspired by Terraform and Terragrunt, locals let you define temporary variables within a single file, reducing repetition and making your configurations more readable and maintainable.

The Problem: Repetition in Stack Configurations

Complex stack configurations often contain repeated values. You might have a naming convention that combines namespace, environment, and stage across multiple components:

# Before: Repetitive and error-prone
components:
terraform:
vpc:
vars:
name: acme-prod-us-east-1-vpc
tags:
Environment: prod
Namespace: acme
eks:
vars:
cluster_name: acme-prod-us-east-1-eks
tags:
Environment: prod
Namespace: acme
rds:
vars:
identifier: acme-prod-us-east-1-rds
tags:
Environment: prod
Namespace: acme

This approach has several problems:

  • Repetition - Same values copied everywhere
  • Inconsistency risk - Easy to mistype or forget to update all occurrences
  • Hard to refactor - Changing a naming convention requires updates in many places

The Solution: File-Scoped Locals

Locals let you define variables once and reference them throughout the file:

# After: Clean and DRY
locals:
namespace: acme
environment: prod
stage: us-east-1
name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}-{{ .locals.stage }}"
tags:
Environment: "{{ .locals.environment }}"
Namespace: "{{ .locals.namespace }}"

components:
terraform:
vpc:
vars:
name: "{{ .locals.name_prefix }}-vpc"
tags: "{{ .locals.tags }}"
eks:
vars:
cluster_name: "{{ .locals.name_prefix }}-eks"
tags: "{{ .locals.tags }}"
rds:
vars:
identifier: "{{ .locals.name_prefix }}-rds"
tags: "{{ .locals.tags }}"

Key Features

Locals Can Reference Other Locals

Locals are resolved in dependency order using topological sorting. You can build complex values from simpler ones:

locals:
namespace: acme
environment: prod
stage: us-east-1
# References other locals - resolved in correct order
name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}"
full_name: "{{ .locals.name_prefix }}-{{ .locals.stage }}"

Circular Dependency Detection

Atmos automatically detects circular dependencies and provides clear error messages:

# This will error with a clear message
locals:
a: "{{ .locals.b }}"
b: "{{ .locals.c }}"
c: "{{ .locals.a }}" # Circular!
atmos describe component vpc -s prod
Error: circular dependency in locals at stacks/prod.yaml

Dependency cycle detected:
a → b → c → a

File-Scoped Isolation

Unlike vars, locals do not inherit across file boundaries via import. This is intentional:

# mixins/region.yaml
locals:
region_prefix: "us-west-2" # Only available in this file

vars:
region: us-west-2 # Inherited by importing files
# stacks/prod.yaml
import:
- mixins/region

# The 'region_prefix' local is NOT available here
# Only 'vars.region' is inherited
locals:
my_prefix: "prod" # This file's own locals

This keeps locals truly local, preventing unexpected interactions between files.

Multi-Level Scopes

Locals can be defined at three levels, with inner scopes inheriting from outer:

  1. Global (stack file root) - Available throughout the file
  2. Component-type (terraform, helmfile, packer sections) - Inherits from global
  3. Component-level (inside component definitions) - Inherits from global + component-type, and from base components via metadata.inherits
# Global locals
locals:
namespace: acme
environment: prod

terraform:
# Terraform-scope locals (inherit from global)
locals:
backend_bucket: "{{ .locals.namespace }}-{{ .locals.environment }}-tfstate"

components:
terraform:
vpc:
vars:
# Uses merged locals (global + terraform section)
name: "{{ .locals.namespace }}-{{ .locals.environment }}-vpc"
bucket: "{{ .locals.backend_bucket }}"

Inspecting Locals with atmos describe locals

To see the resolved locals for any component, use the new describe locals command:

atmos describe locals vpc -s prod-ue2
atmos describe locals vpc -s prod-ue2
components:
terraform:
vpc:
locals:
namespace: acme
environment: prod
stage: us-east-1
name_prefix: acme-prod
full_name: acme-prod-us-east-1
backend_bucket: acme-prod-tfstate

JSON Output for Automation

For scripting and automation, use JSON format:

atmos describe locals vpc -s prod-ue2 --format json
{
"components": {
"terraform": {
"vpc": {
"locals": {
"namespace": "acme",
"environment": "prod",
"name_prefix": "acme-prod"
}
}
}
}
}

Show Locals for a Stack

With the --stack flag (required), show locals for the specified stack:

atmos describe locals --stack prod-ue2
locals:
namespace: acme
environment: prod
terraform:
locals:
backend_bucket: acme-prod-tfstate

The output is in direct stack manifest format - it can be redirected to a file and used as valid YAML.

Why File-Scoped?

You might wonder why locals don't inherit across imports like vars do. The design is intentional:

  1. Predictability - You know exactly what locals are available by looking at the current file
  2. No hidden dependencies - Locals won't mysteriously change based on import order
  3. Safer refactoring - Renaming a local in one file won't break other files
  4. Clear separation - Use vars for values that should propagate; use locals for file-internal convenience

Best Practices

Use locals for DRY configuration within a file

locals:
common_tags:
Team: platform
CostCenter: infrastructure

components:
terraform:
vpc:
vars:
tags: "{{ .locals.common_tags }}"
eks:
vars:
tags: "{{ .locals.common_tags }}"

Build complex values from simple ones

locals:
namespace: acme
environment: prod
region: us-east-1
# Compose complex values
resource_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}-{{ .locals.region }}"
s3_bucket: "{{ .locals.resource_prefix }}-artifacts"
dynamodb_table: "{{ .locals.resource_prefix }}-state-lock"

Keep locals close to their usage

Define locals at the appropriate scope level - don't put everything at the global level:

# Global - used everywhere
locals:
namespace: acme

terraform:
# Terraform-specific - only used by terraform components
locals:
state_bucket: "{{ .locals.namespace }}-tfstate"
vpc_name: "{{ .locals.namespace }}-vpc"

components:
terraform:
vpc:
vars:
# Uses merged locals (global + terraform section)
name: "{{ .locals.vpc_name }}"
bucket: "{{ .locals.state_bucket }}"

Get Started

File-scoped locals are available now. Try them in your stack configurations:

locals:
project: myproject
env: dev

vars:
name: "{{ .locals.project }}-{{ .locals.env }}"

We'd love to hear how you're using locals in your configurations. Share your patterns in GitHub Discussions or open an issue if you encounter any problems.

For usage and configuration, see Configure Locals.