Skip to main content

Configure Locals

The locals section defines file-scoped temporary variables for use within templates. Unlike vars, settings, and env, locals do not inherit across file boundariesβ€”they are resolved within a single file and can reference each other with automatic dependency resolution.

Experimental

Use Cases​

  • Reduce Repetition: Define common values once and reference them throughout the file.
  • Build Complex Values: Construct naming conventions, tags, or resource identifiers from simpler components.
  • Cross-Component References: Use !terraform.state to pull outputs from other components into locals.
  • Conditional Logic: Use Go template conditionals with !env results for environment-aware configuration.

How Locals Work​

Locals are similar to Terraform locals and Terragrunt locals:

  1. File-Scoped: Locals are only available within the file where they are defined. They do not inherit across imports.
  2. Dependency Resolution: Locals can reference other locals using {{ .locals.name }} syntax. Atmos automatically determines the correct resolution order using topological sort.
  3. Cycle Detection: Circular references are detected and reported with clear error messages showing the dependency path (e.g., a -> b -> c -> a).
  4. Template Support: Locals support Go templates with Sprig functions, Gomplate functions, and Atmos template functions.
  5. YAML Functions: Locals support !env, !terraform.state, !store, !exec, and other YAML functions.

Processing Pipeline​

Locals are resolved in a specific order during stack configuration loading:

1. Parse raw YAML β†’ extract locals sections (global, terraform, helmfile, packer)
2. Build dependency graph (extract {{ .locals.X }} references)
3. Topological sort with cycle detection
4. Resolve YAML functions (!env, !terraform.state)
5. Make resolved locals available to templates

YAML functions are always resolved before Go templates. This means you can fetch a value with !env or !terraform.state and then use it in a Go template expression in another local.

Configuration Scopes​

Locals can be defined at three levels within a file, each inheriting from its parent:

Global locals β†’ Component-type locals (terraform/helmfile/packer) β†’ Component-level locals

When the same key exists at multiple levels, the most specific scope wins.

locals:
namespace: acme
environment: prod
name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}"

terraform:
locals:
# Inherits from global, adds terraform-specific locals
backend_bucket: "{{ .locals.namespace }}-{{ .locals.environment }}-tfstate"

components:
terraform:
vpc:
locals:
# Component-specific locals (inherits from global + terraform)
vpc_type: production
vars:
name: "{{ .locals.name_prefix }}-{{ .locals.vpc_type }}-vpc"
bucket: "{{ .locals.backend_bucket }}"

File-Scoped Isolation​

Unlike vars, settings, and env, locals do not inherit across file imports. Each file has its own isolated locals scope.

stacks/catalog/defaults.yaml

# These locals are ONLY available in this file
locals:
default_region: us-east-1

stacks/deploy/production.yaml

import:
- catalog/defaults

# The locals from catalog/defaults are NOT available here
locals:
namespace: acme # Define your own locals

This design ensures predictability β€” you can understand a file without tracing imports.

How does processing work?

Locals are resolved before imports are merged:

  1. Per-File Processing: Each file is processed independently
  2. Locals Resolution: Locals are resolved using only that file's context
  3. Import Merging: Sections (vars, settings, env, components) are merged
  4. Final Template Processing: Templates in other sections use the merged context

Dependency Resolution​

Locals can reference other locals in any order β€” Atmos resolves them automatically:

locals:
full_name: "{{ .locals.name_prefix }}-{{ .locals.component }}" # Resolved last
name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}" # Resolved second
namespace: acme # Resolved first
environment: prod # Resolved first
component: vpc # Resolved first

Circular references are detected and reported with clear error messages showing the dependency cycle.

Accessing Other Sections​

Locals can access settings, vars, and env defined in the same file:

SectionSyntaxDescription
locals{{ .locals.name }}Other locals in the same file
settings{{ .settings.key }}Settings defined in the same file
vars{{ .vars.key }}Variables defined in the same file
env{{ .env.KEY }}Environment variables defined in the same file
settings:
version: v1

vars:
stage: dev

locals:
namespace: acme
label: "{{ .locals.namespace }}-{{ .vars.stage }}-{{ .settings.version }}"
Same-File Access Only

Locals cannot access settings, vars, or env from imported files. If you need values from imports, use vars insteadβ€”they inherit across files.

# ❌ Won't work - imported settings not available to locals
import:
- catalog/defaults # Has settings.region = us-east-1
locals:
region: "{{ .settings.region }}" # Error!

# βœ… Use vars instead - they inherit across imports
vars:
computed_name: "{{ .vars.region }}-cluster"

Using Templates​

Locals support full Go template syntax with Sprig functions, Gomplate functions, and Atmos template functions:

locals:
name: myapp
environment: production
upper_name: '{{ .locals.name | upper }}'
log_level: '{{ if eq .locals.environment "production" }}warn{{ else }}debug{{ end }}'

YAML Functions in Locals​

Locals support all Atmos YAML functions, enabling dynamic value resolution from external sources:

FunctionDescriptionExample
!envEnvironment variables!env API_ENDPOINT
!execCommand execution!exec echo hello
!storeStore lookups!store secrets/db .password
!terraform.stateTerraform state queries!terraform.state vpc .vpc_id
!terraform.outputTerraform outputs!terraform.output vpc .vpc_id

Environment Variables with Conditionals​

Use !env with Go template conditionals to build environment-aware values:

locals:
# Fetch PR number from CI environment (may or may not be set)
pr_number: !env PR_NUMBER

# Conditional: use PR-specific name in CI, default otherwise
deploy_target: '{{ if .locals.pr_number }}preview-{{ .locals.pr_number }}{{ else }}stable{{ end }}'
datastream: '{{ if .locals.pr_number }}ds-pr{{ .locals.pr_number }}{{ else }}ds-main{{ end }}'

components:
terraform:
app:
vars:
deploy_target: "{{ .locals.deploy_target }}"

Cross-Component References​

Use !terraform.state to reference outputs from other components. The 2-argument form uses the current stack automatically:

locals:
# 2-arg form: component + output (uses current stack)
vpc_id: !terraform.state vpc .vpc_id
private_subnets: !terraform.state vpc .private_subnet_ids

# 3-arg form: component + explicit stack + output
shared_vpc_id: !terraform.state vpc shared .vpc_id

# Build derived values from state outputs
cluster_name: "eks-{{ .locals.vpc_id }}"

components:
terraform:
eks:
vars:
vpc_id: "{{ .locals.vpc_id }}"
subnet_ids: "{{ .locals.private_subnets }}"
name: "{{ .locals.cluster_name }}"

Combining YAML Functions with Templates​

YAML functions are resolved first, then Go templates are processed. This allows you to build complex values:

locals:
# Step 1: YAML function fetches the base value
endpoint: !env GRAFANA_ENDPOINT

# Step 2: Go template uses the resolved value
dashboard_url: "https://{{ .locals.endpoint }}/d/ray-workers"
metrics_url: "https://{{ .locals.endpoint }}/api/v1/query"

vars:
monitoring:
dashboard: "{{ .locals.dashboard_url }}"
metrics: "{{ .locals.metrics_url }}"
YAML Function Processing Order

YAML functions in locals are processed during stack configuration loading. The resolved values become available to other locals and component vars through Go templates.

Complex Values​

Locals can contain maps and lists:

locals:
namespace: acme
environment: prod

default_tags:
Namespace: "{{ .locals.namespace }}"
Environment: "{{ .locals.environment }}"
ManagedBy: Atmos

availability_zones:
- us-east-1a
- us-east-1b

vars:
tags: "{{ .locals.default_tags }}"

Component-Level Locals​

Components can define their own locals that inherit from global and section-level locals. They also support inheritance from base components via metadata.inherits:

components:
terraform:
vpc/base:
metadata:
type: abstract
locals:
vpc_type: standard
enable_nat: false

vpc/production:
metadata:
inherits:
- vpc/base
locals:
vpc_type: production # Overrides base
enable_nat: true # Overrides base
vars:
name: "{{ .locals.vpc_type }}-vpc"
nat_enabled: "{{ .locals.enable_nat }}"

Resolution order (later overrides earlier): Global β†’ Section β†’ Base Component β†’ Component

Debugging​

Use atmos describe locals to inspect resolved values:

atmos describe locals -s dev              # All locals for a stack
atmos describe locals vpc -s dev # Locals for a specific component
atmos describe locals -s dev --format json # Output as JSON
Example output
locals:
namespace: acme
environment: dev
name_prefix: acme-dev
terraform:
locals:
backend_bucket: acme-dev-tfstate

Locals vs Vars​

Aspectlocalsvars
ScopeFile-scoped onlyInherits across imports
PurposeTemporary values for DRYInput variables for components
OutputNot passed to componentsPassed to Terraform/Helmfile/Packer
Cross-File AccessCannot access imported valuesCan access merged values from all imports

Use locals for intermediate computations within a single file, and vars for values that need to be passed to components or shared across files.

Template Processing​

When a file defines locals, template processing is automatically enabled. Any {{ ... }} syntax will be processed.

Conflicting Template Syntax

If your YAML files contain Helm templates or other {{ }} syntax, use skip_templates_processing:

import:
- path: catalog/helm-values
skip_templates_processing: true

Error Handling​

Unresolved template references produce clear error messages:

locals:
domain: "{{ .settings.region }}.example.com" # Error if no settings defined
Error: map has no entry for key "settings"

For optional values, use Go template conditionals:

locals:
safe_region: '{{ with .settings }}{{ .region }}{{ else }}us-west-2{{ end }}'

Best Practices​

  1. Extract repetition to locals instead of duplicating values.
  2. Build incrementallyβ€”compose complex values from simpler locals.
  3. Keep locals close to where they're used (component-level when possible).
  4. Use vars for cross-file sharingβ€”locals are file-scoped by design.
  5. Use the 2-arg form of !terraform.state in localsβ€”the current stack is derived automatically.

Try It​

Example: Locals

Reduce repetition and build computed values using file-scoped locals.

Learn more about Locals.

What You'll See

  • Basic locals β€” define reusable values within a file
  • Dependency resolution β€” locals reference other locals, resolved via topological sort
  • Context access β€” locals use {{ .settings.X }}, {{ .vars.X }} from the same file
  • Sprig functions β€” pipe syntax like {{ .locals.namespace | upper }}
  • Complex values β€” maps with templates for resource tags
  • Multiple components β€” the same locals shared across components in a file
  • File-scoped isolation β€” dev.yaml and prod.yaml have independent locals

Try It

cd examples/locals

# View resolved locals for the dev stack
atmos describe locals -s dev

# View resolved locals for a specific component
atmos describe locals myapp -s dev

# See how locals flow into component vars
atmos describe component myapp -s dev

# Compare dev vs prod (same patterns, different values)
atmos describe component myapp -s prod

# Worker component appends a suffix to locals
atmos describe component myapp-worker -s dev

Features Demonstrated

1. Basic Locals and References

locals:
namespace: acme
environment: development
name_prefix: "{{ .locals.namespace }}-{{ .locals.environment }}"

Locals are resolved in dependency order β€” name_prefix waits for namespace and environment.

2. Settings and Vars Access

settings:
version: v1
vars:
stage: dev
locals:
app_version: "{{ .settings.version }}" # β†’ "v1"
stage_name: "{{ .vars.stage }}" # β†’ "dev"

3. Sprig Functions

locals:
namespace_upper: '{{ .locals.namespace | upper }}' # β†’ "ACME"

4. Complex Values (Maps)

locals:
default_tags:
Namespace: "{{ .locals.namespace }}"
Environment: "{{ .locals.environment }}"
Team: "{{ .settings.team }}"
ManagedBy: Atmos

5. File-Scoped Isolation

Each stack file has its own locals. Even though both dev.yaml and prod.yaml define namespace: acme, they are completely separate β€” changing one never affects the other. Locals never propagate across file boundaries via imports.

Key Files

FilePurpose
stacks/deploy/dev.yamlDev stack: all locals features
stacks/deploy/prod.yamlProd stack: same patterns, different values
components/terraform/myapp/main.tfMock Terraform component