Hi @Kellen275,
Unfortunately what you’ve described seems to be a design problem with the module you are calling, which is pushing a bunch of complexity up into your calling module as a result.
The way I’d expect this module to be defined is to declare the variable like this:
variable "instance_refresh" {
description = "If this block is configured, start an Instance Refresh when this Auto Scaling Group is updated"
type = object({
strategy = string
triggers = optional(set(string))
preferences = optional(object({
checkpoint_delay = optional(number)
checkpoint_percentages = optional(list(number))
instance_warmup = optional(number)
max_healthy_percentage = optional(number)
min_healthy_percentage = optional(number)
skip_matching = optional(bool)
auto_rollback = optional(bool)
alarm_specification = optional(object({
alarms = set(string)
}))
scale_in_protected_instances = optional(string)
standby_instances = optional(string)
}))
})
default = null
}
…and then define the dynamic
block based on the presence (non-nullness) of the value:
dynamic "instance_refresh" {
for_each = var.instance_refresh[*]
content {
strategy = instance_refresh.value.strategy
triggers = instance_refresh.value.triggers
dynamic "preferences" {
for_each = instance_refresh.value.preferences[*]
content {
checkpoint_delay = preferences.value.checkpoint_delay
checkpoint_percentages = preferences.value.checkpoint_percentages
instance_warmup = preferences.value.instance_warmup
min_healthy_percentage = preferences.value.min_healthy_percentage
max_healthy_percentage = preferences.value.max_healthy_percentage
auto_rollback = preferences.value.auto_rollback
scale_in_protected_instances = preferences.value.scale_in_protected_instances
skip_matching = preferences.value.skip_matching
standby_instances = preferences.value.standby_instances
}
}
}
}
Your call to the module would then work as you wrote it, because null
would be the correct representation of “not set”. Currently the module seems to treat an empty object as meaning “not set”, which is a non-idiomatic representation that is only possible because the module does not declare what type of value this variable is supposed to accept.
I assume this module is written this way because it was originally written for an old version of Terraform whose language didn’t yet have the facility for optional attributes in object type constraints. The author used some reasonable workarounds for that situation, although using {}
as the default instead of null
was unfortunate and is the main cause of the problem you’ve encountered here.
If possible I think the best solution would be to change the shared module to either fully specify its type constraint as I showed above, or at least to change the default to null
and use the nullness to represent “not set” so that it agrees with the design assumptions of the Terraform language.
If the module author is not willing to make that change then there isn’t really any great option – you’re effectively working around some non-idiomatic module design – but one shorthand that I think of is the following:
instance_refresh = {
for k, v in local.default_instance_refresh : k => v
if local.refresh
}
This is a slightly-questionable use of a for
expression, but it does at least match the slightly-questionable way that the module you are using decides whether you intended to set this argument: if local.refresh
is not true then the for
expression will filter out all of the attributes of local.default_instance_refresh
, leaving you with a value of type object({})
(an object with no attributes) as the module is expecting.
(Using length
on an object type to find out how many attributes it has seems to me essentially symmetrical to using for
expression to dynamically remove attributes from an object value, even though both are quite a strange thing to do.)