Terraform - Iterate over a List of Objects in a Template

Viewed 4902

I'm having issues iterating over a list of objects within a template interpreted by the templatefile function.

I have the following var:

variable "destinations" {
  description = "A list of EML Channel Destinations."

  type = list(object({
    id  = string
    url = string
  }))
}

This is passed in to the templatefile function as destinations. The snippet of template relevant is this:

Destinations:
  %{ for dest in destinations ~}
  - Id: ${dest.id}
    Settings:
      URL: ${dest.url}
  %{ endfor }

When planning Terraform this gives an error of:

Error: "template_body" contains an invalid YAML: yaml: line 26: did not find expected key

I have tried switching the template code to the following:

Destinations:
  %{ for id, url in destinations ~}
  - Id: ${id}
    Settings:
      URL: ${url}
  %{ endfor }

Which gives a different error:

Call to function "templatefile" failed:
../../local-tfmodules/eml/templates/eml.yaml.tmpl:25,20-23: Invalid template
interpolation value; Cannot include the given value in a string template:
string required., and 2 other diagnostic(s).

[!] something went wrong when creating the environment TF plan

I get the impression my iterating over the data type here is somehow incorrect but I cannot fathom how and I cannot find any docs about this at all.

Here is a cut down example of how I'm calling this module:

module "eml" {
  source = "../../local-tfmodules/eml"

  name = "my_eml"

  destinations = [
    {
      id  = "6"
      url = "https://example.com"
    },
    {
      id  = "7"
      url = "https://example.net"
    }
  ]
<cut>
}
3 Answers

I've just found (after crafting a small Terraform module to test templatefile output only) that the original config DOES work (at least in TF v0.12.29).

The errors given are a bit of a Red Herring - the issue is to do with indentation within the template, e.g. instead of:

Destinations:
  %{ for destination in destinations  ~}
  - Id: ${destination.id}
    Settings:
      URL: ${destination.url}
  %{ endfor ~}

it should be:

Destinations:
  %{~ for destination in destinations  ~}
  - Id: ${destination.id}
    Settings:
      URL: ${destination.url}
  %{~ endfor ~}

Notice the extra tilde's (~) at the beginning of the Terraform directives. This makes the Yaml alignment work correctly (you get some lines incorrectly indented and some blank lines). After this the original code in my question works as I expected it to & produces valid yaml.

You can't pass var.destinations as a list of maps to the template. It must be list/set of strings.

But you could do the following:

templatefile("eml.yaml.tmpl", 
          { 
            ids =  [for v in var.destinations: v.id]
            urls =  [for v in var.destinations: v.url] 
          }
    )

where eml.yaml.tmpl is

Destinations:
  %{ for id, url in zipmap(ids, urls)  ~}
  - Id: ${id}
    Settings:
      URL: ${url}
  %{ endfor ~}

Since you are aiming to generate a YAML result, I suggest following the advice in the templatefile documentation about generating JSON or YAML from a template.

Using the yamlencode function will guarantee that the result is always valid YAML, without you having to worry about correctly positioning newlines or quoting/escaping strings that might contain special characters.

Write your templatefile call like this:

templatefile("${path.module}/templates/eml.yaml.tmpl", {
  destinations = var.destinations
})

Then, in the eml.yaml.tmpl, make the entire template be the result of calling yamlencode, like this:

${yamlencode({
  Destinations = [
    for dest in destinations : {
      Id = dest.id
      Settings = {
        URL = dest.url
      }
    }
  ]
})

Notice that the argument to yamlencode is Terraform expression syntax rather than YAML syntax, because in this case it's Terraform's responsibility to do the YAML encoding, and all you need to do is provide a suitable value for Terraform to encode, following the mappings from Terraform types to YAML types given in the yamldecode documentation.

Related