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