Configurable default values on terraform objects
Creating complex configurations requires many dimensions on the input - this post shows how to take care of default values in such a case to offer users of your code fine grained control without enforcing too much input if not required.
Depending on your personal favor you might be using terraform in a sense where you pass all values/variables in a simple way into the code - just by issueing a variable per value.
This works fine for smaller modules or simple configurations. On more complex environments you might end up having the demand for more more sophisticated values that might be passed as an object.
Let's say or example, you want to pass a list of firewall rules - each rule has some mandatory values, like action, priority or maybe the protocol (assuming that most rules might be tcp).
The pattern shown in this post allows the user of your code to override your defaults.
Configurable defaults
Let's take the example with the firewall - you can write a terraform module and enable your users to supply custom default values. This can look like this:
variable "filter_rules_ipv4_defaults" {
type = object({
enabled = optional(bool)
comment = optional(string)
action = optional(string)
chain = optional(string)
protocol = optional(string)
port = optional(string)
interfaces = optional(list(string))
interfaces_negate = bool
source_addresses = optional(list(string))
source_addresses_negate = bool
destination_addresses = optional(list(string))
destination_addresses_negate = bool
priority = number
auto_rule = object({
create_related_deny_rule = bool
create_deny_rule_without_interface_filter = bool
})
})
default = {
priority = 100
interfaces_negate = false
source_addresses_negate = false
destination_addresses_negate = false
auto_rule = {
create_related_deny_rule = false
create_deny_rule_without_interface_filter = false
}
}
nullable = false
description = "Default values for filter rules."
}
variable "filter_rules_ipv4" {
type = map(object({
enabled = optional(bool)
comment = optional(string)
action = string
chain = string
protocol = string
port = optional(number)
interfaces = optional(list(string))
interfaces_negate = optional(bool)
source_addresses = optional(list(string))
source_addresses_negate = optional(bool)
destination_addresses = optional(list(string))
destination_addresses_negate = optional(bool)
priority = optional(number)
auto_rule = optional(object({
create_related_deny_rule = optional(bool)
create_deny_rule_without_interface_filter = optional(bool)
}))
}))
description = "A map of firewall rules"
default = {}
nullable = false
validation {
condition = alltrue([for rule in var.filter_rules_ipv4 : contains(["input", "forward", "output"], rule.chain)])
error_message = "Invalid chain value. Must be either 'input', 'forward' or 'output'."
}
validation {
condition = alltrue([for rule in var.filter_rules_ipv4 : contains(["accept", "drop", "reject"], rule.action)])
error_message = "Invalid protocol value. Must be either 'accept', 'drop' or 'reject'."
}
validation {
condition = alltrue([for rule in var.filter_rules_ipv4 : rule.port == null || try(rule.port >= 0 && rule.port <= 65535, false)])
error_message = "Invalid port value. Must be between 0 and 65535."
}
}
example code for customizable defaults
This will provide two variables:
- One that holds the firewall rules as a map
- One that holds the default values as a single object
Now, to merge the values, the rules will be iterated and for every iteration there will be a merge - either using terraform's native merge or for example the deepmerge module (registry.terraform.io/isometry/deepmerge)
The merge then looks like this:
user_rules_input_ipv4 = {
for k, v in var.filter_rules_ipv4 : k => provider::deepmerge::mergo(var.filter_rules_ipv4_defaults, { for mk, mv in v : mk => mv if mv != null })
}
merging values
This pattern is quite powerful, especially if a user should be able to customize the defaults on invocation to not be forced to specifiy all values over and over again in the map/list of inputs. My next post will cover another approach.