Skip to main content

!template

The !template Atmos YAML function is used to handle the outputs containing maps or lists returned from the 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.

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")) }}'