Using terraform yamldecode to access multi level element

Viewed 10475

I have a yaml file (also used in a azure devops pipeline so needs to be in this format) which contains some settings I'd like to directly access from my terraform module.

The file looks something like:

variables:
  - name: tenantsList
    value: tenanta,tenantb
  - name: unitName
    value: canary

I'd like to have a module like this to access the settings but I can't see how to get to the bottom level:

locals {
  settings = yamldecode(file("../settings.yml"))
}

module "infra" {
  source = "../../../infra/terraform/"
  unitname = local.settings.variables.unitName
}

But the terraform plan errors with this:

Error: Unsupported attribute

  on canary.tf line 16, in module "infra":
  16:   unitname  = local.settings.variables.unitName
    |----------------
    | local.settings.variables is tuple with 2 elements

This value does not have any attributes.
2 Answers

It seems like the main reason this is difficult is because this YAML file is representing what is logically a single map but is physically represented as a YAML list of maps.

When reading data from a separate file like this, I like to write an explicit expression to normalize it and optionally transform it for more convenient use in the rest of the Terraform module. In this case, it seems like having variables as a map would be the most useful representation as a Terraform value, so we can write a transformation expression like this:

locals {
  raw_settings = yamldecode(file("${path.module}/../settings.yml"))
  settings = {
    variables = tomap({
      for v in local.raw_settings.variables : v.name => v.value
    })
  }
}

The above uses a for expression to project the list of maps into a single map using the name values as the keys.

With the list of maps converted to a single map, you can then access it the way you originally tried:

module "infra" {
  source = "../../../infra/terraform/"
  unitname = local.settings.variables.unitName
}

If you were to output the transformed value of local.settings as YAML, it would look something like this, which is why accessing the map elements directly is now possible:

variables:
  tenantsList: tenanta,tenantb
  unitName: canary

This will work only if all of the name strings in your input are unique, because otherwise there would not be a unique map key for each element.


(Writing a normalization expression like this also doubles as some implicit validation for the shape of that YAML file: if variables were not a list or if the values were not all of the same type then Terraform would raise a type error evaluating that expression. Even if no transformation is required, I like to write out this sort of expression anyway because it serves as some documentation for what shape the YAML file is expected to have, rather than having to study all of the references to it throughout the rest of the configuration.)

Try out my self implemented module. You can open multiple YAML files in one run with it. Place this somewhere, where you like to access one or multiple YAML files.

Usage

module "yaml" {
  source  = "levmel/yamldecode/multiple"
  version = "0.0.1"
  filepaths = ["routes/nsg_rules.yaml.", "network/private_endpoints/*.yaml"]
}

You should change the filepaths with the relative paths of your YAML files. If you like to select all YAML files in a folder, then you should do it with a "*" before the ".yaml" format. (see above)

Pattern to access entries in a YAML file:

module.yaml.files.[name_of_your_file_without_format].entry


Pattern to write relative paths:

For all YAML files in a folder structure use this "folder/../*.yaml". For specific YAML file in a folder structure use this "folder/../name_of_yaml.yaml"

Example

routes/nsg_rules.yaml

aks:
  rdp:
    name: rdp
    priority: 80
    direction: Inbound
    access: Allow
    protocol: Tcp
    source_port_range: "*"
    destination_port_range: 3389
    source_address_prefix: VirtualNetwork
    destination_address_prefix: "*"
  ssh:
    name: ssh
    priority: 70
    direction: Inbound
    access: Allow
    protocol: Tcp
    source_port_range: "*"
    destination_port_range: 24
    source_address_prefix: VirtualNetwork
    destination_address_prefix: "*"

Access entry in nsg_rules.yaml:

module "yaml" {
  source  = "levmel/yamldecode/multiple"
  version = "0.0.1"
  filepaths = ["routes/nsg_rules.yaml."]
}

output "test" {
  value = module.yaml.files.nsg_rules.aks.rdp.name
}

Output is: "test" = "rdp"

Related