Skip to content

Latest commit

 

History

History
237 lines (171 loc) · 8.58 KB

File metadata and controls

237 lines (171 loc) · 8.58 KB
title !template
sidebar_position 9
sidebar_label !template
sidebar_class_name command
description Handle outputs containing maps or lists returned from the atmos.Component template function

import Intro from '@site/src/components/Intro'

The `!template` Atmos YAML function is used to [handle the outputs containing maps or lists](/functions/template/atmos.Component#handling-outputs-containing-maps-or-lists) returned from the [`atmos.Component`](/functions/template/atmos.Component) template function.

Usage

# Process the output of type list from the `atmos.Component` template function in the provided stack
var1: !template '{{ toJson (atmos.Component "<component>" "<stack>").outputs.test_list }}'

# Process the output of type map from the `atmos.Component` template function in the current stack
var2: !template '{{ toJson (atmos.Component "component1" .stack).outputs.test_map }}'

Why !template is needed?

You can use the atmos.Component template function to read outputs (remote state) from Terraform/OpenTofu components, and use those in your stack manifests.

When the output of the atmos.Component function is a simple type (string or number), it's correctly handled in YAML, and is sent to the Terraform/OpenTofu component as a simple type.

For example, this function:

var1: '{{ (atmos.Component "<component>" "<stack>").outputs.test_string }}'

produces the following result:

var1: test

When the outputs are complex types (list or map):

var1: '{{ toJson (atmos.Component "<component>" "<stack>").outputs.test_list }}'
var2: '{{ toJson (atmos.Component "component1" "<stack>").outputs.test_map }}'

we'll get the following results:

var1: '["item_1","item_2","item_3"]'
var2: '{"a":1,"b":2,"c":3}'

Because the template expressions are quoted, the results are JSON-encoded strings, not objects.

The results can be sent to the var1 and var2 Terraform variables, but the variables need to be of type string, and you'll have to decode the strings into Terraform list and map using the jsondecode function in your Terraform code. In many cases, this is not an acceptable solution because the Terraform variables var1 and var2 are already of type list and map, and you can't (or don't want to) change the Terraform code to convert them into strings.

We can try to un-quote the template expressions:

var1: {{ toJson (atmos.Component "<component>" "<stack>").outputs.test_list }}
var2: {{ toJson (atmos.Component "component1" "<stack>").outputs.test_map }}

but it does not work because it's not a valid YAML. In YAML, curly braces { } are used to denote a JSON-like inline mapping, which corresponds to a map or dictionary in YAML, and the double curly braces are not valid in YAML.

We can try to use YAML multiline strings with the block style indicator, and un-quote the templates:

var1: >-
  {{ toJson (atmos.Component "<component>" "<stack>").outputs.test_list }}

var2: >-
  {{ toJson (atmos.Component "component1" "<stack>").outputs.test_map }}

but it still generates the same result (JSON-encoded strings, not JSON objects):

var1: |
  ["item_1","item_2","item_3"]

var2: |
  {"a":1,"b":2,"c":3}

The !template Atmos YAML function to the rescue!

The !template YAML function receives the result from the atmos.Component and toJson functions, and converts it into the complex types (list or map) by decoding the JSON strings.

The following !template function calls:

var1: !template '{{ toJson (atmos.Component "<component>" "<stack>").outputs.test_list }}'
var2: !template '{{ toJson (atmos.Component "component1" .stack).outputs.test_map }}'

generates the following YAML:

var1:
  - item_1
  - item_2
  - item_3

var2:
  a: 1
  b: 2
  c: 3

The results are correct list and map YAML types, and can be sent to the Terraform component without modifying the types of its input variables.

:::tip

When reading Atmos components outputs (remote state) in Atmos stack manifests, instead of using the three functions atmos.Component, toJson and !template, use the !terraform.output YAML function. It produces the same results, correctly handles the complex types (lists and maps), and has a much simpler syntax.

:::

Deferred Evaluation During Merge

Atmos uses deferred evaluation when processing YAML functions during configuration merging. This prevents type conflicts that can occur when inheriting and overriding values across multiple stack layers.

The Problem: Type Conflicts

Without deferred evaluation, mixing concrete values and YAML functions in the inheritance chain would cause errors:

# Base catalog (catalog/vpc/defaults.yaml)
components:
  terraform:
    vpc:
      vars:
        security_group_ids: ["sg-111", "sg-222"]  # Concrete list value

# Environment override (stacks/prod/networking.yaml)
components:
  terraform:
    vpc:
      vars:
        security_group_ids: !template '{{ toJson (atmos.Component "security" .stack).outputs.group_ids }}'  # YAML function - different type!

In this scenario, Atmos would attempt to merge a list (["sg-111", "sg-222"]) with a YAML function reference during the configuration merge process, resulting in a type mismatch.

The Solution: Deferred Merge

Atmos now defers the evaluation of YAML functions (including !template, !terraform.output, !terraform.state, !store, !store.get, !exec, and !env) until after all configuration layers have been merged. This three-phase approach eliminates type conflicts:

  1. Defer Phase - YAML functions are identified and temporarily replaced with placeholders
  2. Merge Phase - All configuration layers merge without type conflicts
  3. Evaluate Phase - YAML functions are evaluated and applied to the final merged result

With deferred evaluation, the example above works seamlessly. The YAML function in the override layer is deferred during merge, then evaluated after merging completes, correctly replacing the base value.

Benefits

  • Flexible Configuration Patterns - Mix static values and YAML functions across inheritance layers without conflicts
  • Gradual Migration - Migrate from static to dynamic configurations incrementally
  • Team Collaboration - Different teams can use different approaches (static vs. templated) in their layers
  • Multi-Environment Support - Use static values in dev/staging and YAML functions in production

Advanced Examples

The !template Atmos YAML function can be used to make your stack configuration DRY and reusable.

For example, suppose we need to restrict the Security Group ingresses on all components provisioned in the infrastructure (e.g. EKS cluster, RDS Aurora cluster, MemoryDB cluster, Istio Ingress Gateway) to a specific list of IP CIDR blocks.

We can define the list of allowed CIDR blocks in the global settings section (used by all components in all stacks) in the allowed_ingress_cidrs variable:

settings:
  allowed_ingress_cidrs:
    - "10.20.0.0/20"  # VPN 1
    - "10.30.0.0/20"  # VPN 2

We can then use the !template function with the following template in all components that need their Security Group to be restricted:

# EKS cluster
# Allow ingress only from the allowed CIDR blocks
allowed_cidr_blocks: !template '{{ toJson .settings.allowed_ingress_cidrs }}'
# RDS cluster
# Allow ingress only from the allowed CIDR blocks
cidr_blocks: !template '{{ toJson .settings.allowed_ingress_cidrs }}'
# Istio Ingress Gateway
# Allow ingress only from the allowed CIDR blocks
security_group_ingress_cidrs: !template '{{ toJson .settings.allowed_ingress_cidrs }}'

The !template function and the '{{ toJson .settings.allowed_ingress_cidrs }}' expression allows you to use the global allowed_ingress_cidrs variable and the same template even if the components have different variable names for the allowed CIDR blocks (which would be difficult to implement using Atmos inheritance or other Atmos design patterns).

:::tip To append additional CIDRs to the template itself, use the list and Sprig concat functions:

allowed_cidr_blocks: !template '{{ toJson (concat .settings.allowed_ingress_cidrs (list "172.20.0.0/16")) }}'

:::