Terraform templating and loops

You probably already know it’s not easily possible to loop through a variable of type slice/list within templates, in fact, there are no loop constructs within TF templates at all at this point in time.

There is hope though, according to their comment on this GH issue: https://github.com/hashicorp/terraform/issues/16628

So, say we want to generate a JSON file from a slice/array that, once finished, looks something like this :

We cannot simply iterate over an array to change the value of endpointX as we cannot loop through it.

What we’d expect to be able to do, in the template, is something like this

we’d have to handle that last , but let’s ignore that for now — the idea is simple, iterate over values in the array of map endpoints and use each key and value as variables within that loop to generate the json file. As we already know, this isn’t possible, and we won’t get this anytime soon.

Luckily, necessity is the mother of invention, and I got this to work with the existing loop constructs outside the template; although it’s not as “clean” as above, it’s usable and produces the desired result.

Using TFs buit-in loop construct

To make use of TFs built-in loop construct and generate the wanted json, we will need 2 template files, the first will look like this

// data.json.tmpl
{
"name": "${name}",
"endpoint": "${endpoint}"
}

and the second template looks like this

// service.json.tmpl
[
${value},
{"links": ${links}}
]

Maybe you already know where we’re heading with this, we now render the first template using the built-in loop constructs from TF using the template_file data resource

variable "endpoints" {
type = "list"
default = [
{ endpoint1 = "https://endpoint-1.example.com" },
{ endpoint2 = "https://endpoint-2.example.com" },
{ endpoint3 = "https://endpoint-3.example.com" },
]
}
data "template_file" "data_json" {
template = "${file("${path.module}/data.json.tmpl")}"
count = "${length(var.endpoints)}"
vars {
endpoint = "${element(values(var.endpoints[count.index]), 0)}"
name = "${element(keys(var.endpoints[count.index]), 0)}"
}
}

and then re-use the .rendered output as input to the 2nd template, which really just generates the list by pre- and post- fixing the $value variable with [] .

variable "links" {
type = "list"
default = [
"link1",
"link2",
"link3",
]
}
data "template_file" "service_json" {
template = "${file("${path.module}/service.json.tmpl")}"
vars {
value = "${join(",", data.template_file.data_json.*.rendered)}"
links = "${jsonencode(var.links)}"
}}

and finally we’re using the output module to show us what we’ve done, this is also how you’d use the result as input to any other TF module that accepts json.

output "json" {
value = "${data.template_file.service_json.rendered}"
}

and the output

$ tf apply
data.template_file.data_json[2]: Refreshing state...
data.template_file.data_json[0]: Refreshing state...
data.template_file.data_json[1]: Refreshing state...
data.template_file.service_json: Refreshing state...
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
json = [
{
"name": "endpoint1",
"endpoint": "https://endpoint-1.example.com"
},{
"name": "endpoint2",
"endpoint": "https://endpoint-2.example.com"
},{
"name": "endpoint3",
"endpoint": "https://endpoint-3.example.com"
},
{"links": ["link1","link2","link3"]}
]

Converting lists/slices to JSON

You might have noticed the jsonencode in the last part, this helps to convert any structure to valid json and it becomes valid input to the template as json in essence is just text

variable "links" {
type = "list"
default = [
"link1",
"link2",
"link3",
]
}
data "template_file" "service_json" {
[...]
links = "${jsonencode(var.links)}"
}}

References