Terraform Template each list value

1.5k Views Asked by At

I have a TF module (TF 1.2.8) which creates a vSphere VM. In the metadata template file I want to have it so I can specify X number of nameservers. The current template looks like this:-

metadata.tftpl

instance-id: ${vm_name}
local-hostname: ${vm_name}
network:
  version: 2
  ethernets:
    ens192:
      dhcp4: false
      addresses:
        - ${vm_ip}
      gateway4: ${vm_gateway}
      nameservers:
        addresses:
        %{ for addr in vm_nameservers ~}
          - ${addr}
        %{ endfor ~}

and the template block for this is:-

data "template_file" "metadata" {
  template = "${file("${path.module}/templates/metadata.tftpl")}"
  vars = {
    vm_name = var.vm_name
    vm_ip = var.vm_ip
    vm_gateway = var.vm_gateway
    vm_nameservers = var.vm_nameservers
  }
}

with the variable declarations being

variable "vm_nameservers" {
  type = list
}

and an example of how I want to lay it out in the tfvars file:

vm_nameservers = ["10.10.0.2", "10.10.0.3"]

I've tried looking through the docs for this but there doesn't seem to be anything obvious that would solve the issue, I think I may have declared the variable type incorrectly.

When I run a plan I get the following error:-

Error: Incorrect attribute value type
│ 
│   on .terraform/modules/vm/main.tf line 37, in data "template_file" "metadata":
│   37:   vars = {
│   38:     vm_name = var.vm_name
│   39:     vm_ip = var.vm_ip
│   40:     vm_gateway = var.vm_gateway
│   41:     vm_nameservers = var.vm_nameservers
│   42:   }
│     ├────────────────
│     │ var.vm_gateway is a string, known only after apply
│     │ var.vm_ip is a string, known only after apply
│     │ var.vm_name is a string, known only after apply
│     │ var.vm_nameservers is a list of dynamic, known only after apply
│ 
│ Inappropriate value for attribute "vars": element "vm_nameservers": string
│ required.
╵

3

There are 3 best solutions below

0
Leo On

Honestly I didn't understand the scenario you try to explain, but there are 2 ways to create multiple resources of the same type: count or for_each.

Count would go as:

count = length(your_list) *or number
#to access the values you would use:
your_list[count.index].value

or for_each:

for_each = toset(your object)
#to access the values you would use:
each.key

So I guess it would be something like

data "template_file" "metadata" {
  template = "${file("${path.module}/templates/metadata.tftpl")}"
}

resource "whatever" "this"{
  count = length(data.template_file.metadata)
  vm_name = data.template_file.metadata[count.index].vm_name
  vm_ip = data.template_file.metadata[count.index].vm_ip
  vm_gateway = data.template_file.metadata[count.index].vm_gateway
  vm_nameservers = data.template_file.metadata[count.index].vm_nameservers
}

PS: This is just an example based on the concepts of how to use count and for_each, the code above should not work as there is no resource "whatever"

0
Marko E On

I am not sure how is this code used in a resource, but I would strongly suggest moving away from the data source, and switching to templatefile built-in function [1]. The example I will give creates a local YML file based on the template file that you provided:

resource "local_file" "metadata" {
  filename = "${path.root}/metadata.yml"
  content = templatefile("${path.root}/metadata.yml.tftpl", {
    vm_name        = "somevmname"
    vm_ip          = "ip.add.re.ss"
    vm_gateway     = "gateway"
    vm_nameservers = var.vm_nameservers
  })
}

The variables for the templated file have been given dummy names. The last part is changing the template file (metadata.tftpl):

instance-id: ${vm_name}
local-hostname: ${vm_name}
network:
  version: 2
  ethernets:
    ens192:
      dhcp4: false
      addresses:
        - ${vm_ip}
      gateway4: ${vm_gateway}
      nameservers:
        addresses:
%{ for addr in vm_nameservers ~}
          - ${addr}
%{ endfor ~}

Terraform plan gives the following output:

  # local_file.metadata will be created
  + resource "local_file" "metadata" {
      + content              = <<-EOT
            instance-id: somevmname
            local-hostname: somevmname
            network:
              version: 2
              ethernets:
                ens192:
                  dhcp4: false
                  addresses:
                    - ip.add.re.ss
                  gateway4: gateway
                  nameservers:
                    addresses:
                      - 10.10.0.2
                      - 10.10.0.3
        EOT
      + directory_permission = "0777"
      + file_permission      = "0777"
      + filename             = "./metadata.yml"
      + id                   = (known after apply)
    }

[1] https://developer.hashicorp.com/terraform/language/functions/templatefile

0
Martin Atkins On

You are currently using the obsolete hashicorp/template provider that was originally built for much older versions of Terraform that didn't yet have built-in template support. Those older versions of Terraform also didn't support passing mixtures of different types of elements into an argument of a resource type and so this provider allows only string values for your variables.

The modern way to render external template files in Terraform is to use the built-in templatefile function, which is not constrained by the limitations of obsolete versions of Terraform.

You can replace your data "template_file" block with a local value whose expression is just a call to the templatefile function:

locals {
  metadata = templatefile("${path.module}/metadata.yml.tftpl", {
    vm_name        = var.vm_name
    vm_ip          = var.vm_ip
    vm_gateway     = var.vm_gateway
    vm_nameservers = var.vm_nameservers
  })
}

The second argument to templatefile is an object whose attributes correspond with variable names available in the template. The values of those attributes can be of any Terraform language type, including lists as with your var.vm_nameservers value.


Some other notes, tangential to your question but might address some downstream problems after you fix your immediate problem:

  • Please consider the advice in Generating JSON or YAML from a template; using Terraform's built-in yamlencode function will guarantee valid YAML syntax without any special attention to the exact layout of spaces and newline characters in your template.
  • The error message says that var.vm_nameservers is a "list of dynamic", which suggests that you haven't declared its type constraint properly. The way your template is written requires that the elements of this list be strings, so you should set type = list(string) in the variable "vm_nameservers" block to guarantee that.