Hi @harshavmb,
As you’ve now seen, when converting a value to an object type, Terraform will produce a successful conversion as long as the given object has at least the attributes specified, and it will discard any additional attributes in order to make the result have the required type. In your case, the conversion is happening at the boundary of the module itself: the calling module provides a value for zones
, and Terraform converts that to match your given type constraint in order to populate var.zones
in your module.
The type object({})
is an object with no attributes at all, so Terraform can convert any object to that type but it will do so by discarding all of the attributes, which is indeed what the error message says is happening here:
| each.value is object with no attributes
If you were to inspect var.zones
here, and if we were using the default
value given in the declaration, we’d see that its value is:
{
test0 = {}
test1 = {}
}
…and so for_each
over that will indeed set each.value
to an empty object on every repetition, because that’s what this map contains.
Terraform object types are designed to enforce a particular structure so that the calling module’s value can be checked to match that structure, and so the best way to use an object type constraint is to enumerate all of the attributes you are expecting:
type = map(
object({
vpc_ids = set(string)
force_destroy = bool
tags = map(string)
})
)
You mentioned that removing the type constraint altogether also let it work. That’s because the default type constraint is any
, which asks Terraform to choose the type automatically based on the provided value. The chosen type for the default you showed here would actually be an object type rather than a map type, like this:
object({
test0 = object({
vpc_ids = tuple(string, string)
force_destroy = bool
tags = object({
test = string
})
})
test1 = object({
vpc_ids = tuple(string)
force_destroy = bool
tags = object({
test = string
zone_name = string
})
})
})
Because Terraform can automatically convert from tuple and object types to list/set and map types (as long as the values inside have homogeneous types), you can generally get away with that sort of automatic type selection, though you will lose the benefit of having Terraform automatically check the type of the value given by the caller and so if they get it wrong they will likely get a less helpful error from inside the implementation of the module.
At the time of writing the latest version of Terraform has an experimental feature Custom Validation Rules for variables, which allows expressing validation rules using normal expressions rather than just type constraints, and so it can represent more complex validation rules than the type system alone can define. For example:
variable "zones" {
type = any
validation {
condition = can(tomap(var.zones))
error_message = "The \"zones\" value must be a map of objects."
}
validation {
condition = can([for v in var.zones : toset(v.vpc_ids)])
error_message = "All \"zones\" objects must have \"vpc_ids\", a set of VPC ids."
}
validation {
condition = can([for v in var.zones : tobool(try(v.force_destroy, false))])
error_message = "The \"zones\" objects' \"force_destroy\" must be boolean."
}
validation {
condition = can([for v in var.zones : tomap(try(v.tags, {}))])
error_message = "The \"zones\" objects' \"tags\" must be maps."
}
}
locals {
# A normalized version of var.zones for convenient use without
# lots of preconditions elsewhere in the configuration.
zones = tomap({
for k, v in var.zones : k => {
vpc_ids = toset(v.vpc_ids),
force_destroy = tobool(try(v.force_destroy, false)),
tags = tomap(try(v.tags, {}))
}
})
}
The validation feature might change before it finally ships as non-experimental, but the idea of it is to retain the same benefit of checking the value in the calling module
block, but to give some more flexibility in what values can be accepted. The validation
blocks take the place of the type check, and the local.zones
expression takes the place of the automatic type conversion that would normally be done, allowing you to customize its behavior to provide default values for sub-attributes, etc.
Although this feature isn’t ready to use in “real” configurations today (due to being experimental), I think it will be a nice answer to this question in the future once it has stablized.