Upgrade to terraform 0.12.x, passing list or map variable to resource no longer works

It is common for use to define a list or a map as a variable:

variable “maintenance_window” {
default = [{
day = “1”
hour = “3”
update_track = “stable”
}]

and then refer to the variable in the resource:

resource “google_sql_database_instance” “db” {
maintenance_window = “${var.maintenance_window}”
}

However when running terraform(0.12.8) plan we get this error:

An argument named “maintenance_window” is not expected here. Did you mean to
define a block of type “maintenance_window”?

I’ve read the upgrade documentation but don’t see this referred to directly.
How is this done in terraform 12?

Hi @dodizzle!

If you use the automatic upgrade tool described in the v0.12 Upgrade Guide it should update this configuration automatically, but I’ll also describe here how you can update this manually in case you’ve already migrated parts of your configuration such that the upgrade tool can’t be used anymore. One advantage of migrating this manually is that we can make some adjustments to improve the interface using Terraform 0.12 features, which the automatic upgrade tool doesn’t have the intuition to do.

What you’ve encountered here is the problem described in the upgrade guide section Attributes vs. blocks. maintenance_window is a nested block type rather than an attribute, so it’s not valid to define it using the attribute syntax.

It looks like maintenence_window here is a singleton nested block, so it’d be more natural to represent it as a single objcet that can be null rather than as a list. We’d declare that like this:

variable “maintenance_window” {
  type = object({
    day          = number
    hour         = number
    update_track = string
  })
  default = {
    day          = 1
    hour         = 3
    update_track = "stable"
  }
}

resource "google_sql_database_instance" "db" {
  # (...any other arguments you need...)

  dynamic "maintenance_window" {
    # The [*] syntax here transforms this value into
    # either a single-element list or a zero-length
    # list, depending on whether the value is null.
    # That means there will be no maintenance_window
    # blocks defined if the variable is set to null.
    for_each = var.maintenance_window[*]
    content {
      day          = maintenance_window.value.day
      hour         = maintenance_window.value.hour
      update_track = maintenance_window.value.day
    }
  }
}

Now the caller can optionally set the variable to null in order to disable the maintenance window, assuming that’s something valid to do with this resource type. (I’m not familiar with the google_sql_database_instance resource type in particular, so I’m not sure if this block is required or not.)

module "example" {
  # ...

  maintenance_window = {
    day          = 5
    hour         = 2
    update_track = "stable"
  }
}

In return for explicitly declaring the object type of this variable, Terraform will validate the expression in the module block to ensure it matches the type constraint, and report the error inside the module block if not, rather than returning a confusing error message referring to the child module.