About consistency of variable types

Hi all,

Using terraform 1.0.5:

$ terraform --version
Terraform v1.0.5
on linux_amd64

Your version of Terraform is out of date! The latest version
is 1.0.7. You can update by downloading from https://www.terraform.io/downloads.html

1.0.7 is not yet available on my system.

Using that input file:

platforms_desc = {
  fake_main = {
    name            = "fake_main",
    is_root         = true,
    is_main         = true
  },
  fake_sub = {
    name            = "fake_sub",
  }
}

is_main does not use quotes, I would expect that data shall contain a boolean value.

In that main.tf:

variable "platforms_desc" {
  type        = map(any)
  description = ""
  default     = {}
}

module "input_data_mgmt" {
  source    = "./module"

  platforms_description         = var.platforms_desc
}

output "main_platform" {
  value = module.input_data_mgmt.main_platform
}

With that code in module/main.tf:

variable "platforms_description" {
  type        = map(any)
  description = ""
  default     = {}
}

locals {
  // NOTE: to get at least root folder for later uses
  main_platform     = {
    for name, object in var.platforms_description : name => {
      name              = name,
      is_root           = lookup(object, "is_root", false)
      is_main           = object.is_main
    } if lookup(object, "is_main", false) == "true"
  }
}

output "main_platform" {
  value = local.main_platform
}

When trying the value of is_main the test must be done using string format

I get that output:

$ terraform apply -var-file=input.tfvars 

Changes to Outputs:
  + main_platform = {
      + fake_main = {
          + is_main = "true"
          + is_root = "true"
          + name    = "fake_main"
        }
    }

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

In this output the is_main data (as “is_root”) is shown as a string.

Removing the quotes from the test as follow:

locals {
  // NOTE: to get at least root folder for later uses
  main_platform     = {
    for name, object in var.platforms_description : name => {
      name              = name,
      is_root           = lookup(object, "is_root", false)
      is_main           = object.is_main
    } if lookup(object, "is_main", false) == true
  }
}

Gives the following output:

terraform apply -var-file=input.tfvars 

Changes to Outputs:
  + main_platform = {}

You can apply this plan...

Here main_platform is {} empty as the test on boolean didn’t matched any object.

Changing

  type        = map(any) 

to

  type        = map(object({
    name = string,
    is_root = bool,
    is_main = bool
  }))

in both variables declaration (root + module), terraform complains with the following:

The given value is not valid for variable "platforms_desc": element "fake_sub": attributes "is_main" and "is_root" are required.

So, using that syntax removes the possibility to not declare each and every element of objects. And this simplify drastically input files.

So, I’m puzzled.

Is it safe, official, to consider all variables contents as strings, always? Or perhaps only once the variable is passed to some module? I did not made this test yet but tests may be numerous…

Best regards,

mathias

Hi @mathias213,

If you write map(any) as a type constraint then you are asking Terraform to automatically infer a specific type to use instead of the any placeholder. Terraform will therefore analyze all of the elements of the provided value to find some type they have in common which would therefore be able to replace the any keyword in the type constraint.

The concrete original type of the platforms_desc value you showed in your question would be the following:

object({
  fake_main = object({
    name    = string
    is_root = bool
    is_main = bool
  })
  fake_sub = object({
    name = string
  })
})

Terraform can automatically convert from an object type to a map type only if all of the attribute values in the object can convert to the map’s element type. You used the any placeholder to ask Terraform to automatically select a map element type here, and so Terraform will study the type of fake_main and fake_sub and try to find a type that both values could convert to.

These two nested object types aren’t naturally compatible, and so ideally Terraform would’ve returned an error here telling you to make them compatible. For example, you could’ve made them both have the same object types by writing out is_root and is_main attributes in the fake_sub object:

platforms_desc = {
  fake_main = {
    name    = "fake_main",
    is_root = true,
    is_main = true
  },
  fake_sub = {
    name    = "fake_sub",
    is_root = false
    is_main = false
  }
}

Now both of those attributes have the same type, and so Terraform can replace any with that object type, giving the following inferred type:

map(object({
  name    = string
  is_root = bool
  is_main = bool
}))

An unfortunate point of confusion here is that for compatibility with Terraform v0.11 and earlier, Terraform has some additional type conversion rules to go from bool to string and from number to string. Because your two attributes had different object types, rather than failing outright Terraform looked one level deeper and tried to find some type the nested objects could both convert to. Terraform then noticed that it can convert bool to string and therefore both of those object types can convert to map(string) by converting the two boolean values to string values.

The final inferred type then, after this recursive work to find successive type conversions to satisfy the constraint, is:

map(map(string))

In other words, Terraform selected map(string) as the concrete type to use where you wrote any, and then converted each of the elements to that type in order to produce a result.

You can avoid this confusing situation of Terraform trying to automatically infer types by writing out exactly the type you want, without using any. In that case, Terraform will convert the value to exactly the type you specified or return an error explaining why it couldn’t.

It sounds like you got here because you were looking for a way to avoid writing out the is_root and is_main attributes inside some of the objects, but that isn’t possible because that would make the objects all be of different types, and so there’s no single element type to select for the map.

For now, using a consistent set of attributes and attribute types on all of your elements is a requirement, so your final attempt is the best path to take.

There is some movement toward what you wanted in the “optional attributes” experiment, though the first iteration of this generated some feedback which is causing us to not stabilize the initial experiment and instead wait for at least one more experimental iteration before stabilizing. Therefore I would not suggest using this experimental feature as currently described, but if you write out your object types consistently today (specifying all of the attributes for each one) then you’ll be able to adopt the optional attributes mechanism for your module later without breaking compatibility with existing callers.

Hi @apparentlymart,

Thank you for this detailed answer. I’m still working on rewriting things using optional() which is very useful. I shall come back for a better answer ; )

Cheers,

mathias