Override a single value in a map

I would like to know if it is possible to merge two map of maps without replacing the main map object.

My map object is defined as follows:

variable "apps" {
type = map(object({
  is_enabled    = bool
 cost_center   = string
}))
default = {}
}

locals {
  default_apps = {
    "api-1" = {
         is_enabled   = false
         cost_center  = "1234"
     },
    "api-2" = {
         is_enabled   = false
         cost_center  = "1235"
    },
 }
apps = merge(
     local.default_apps,
     var.apps
   )
}

I would like to change the value of api-1[‘s_enabled’] to true.

If define my tfars as follows:

  apps = {
    "api-1" = {
      is_enabled   = true
    }
  }

I get the following error:

Error: Invalid value for input variable

The environment variable TF_VAR_apps does not contain a valid value for
variable "apps": element "api-1": attribute "cost_center" is required.

It works if I define my tfvars like so:

  apps = {
    "api-1" = {
      is_enabled   = true
      cost_center  = "1234"
    }
  }

My goal is to override a single value of one of the pre defined local variables under default_apps (e.x is_enabled) in tfvars.

Hi @varig203,

It seems like your intent here is to merge each of the elements of var.apps and of local.default_apps separately, so that var.apps can optionally override parts of the objects in local.default_apps. It may work out simpler to instead define a variable apps_enabled which is a map(bool) that is merged specifically into the is_enabled attribute, but I’ll show an example that I think will support the generality you were hoping for here, at the expense of some increased complexity:

locals {
  apps = tomap({
    for k in setunion(keys(local.default_apps), keys(var.apps)) : k => {
      is_enabled = tobool(coalesce(
        try(var.apps[k].is_enabled, null),
        try(local.default_apps[k].is_enabled, null),
      ))
      cost_center = tostring(coalesce(
        try(var.apps[k].cost_center, null),
        try(local.default_apps[k].cost_center, null),
      ))
    }
  })
}

This is using several different Terraform language features together:

  • for expressions to create a new mapping with an element for each element of some other collection.
  • keys to get all of the keys defined in a map.
  • setunion to merge the keys from both of your maps together into a single set of keys.
  • coalesce to select the first non-null value from a series of values.
  • try to concisely handle the situation where a particular key isn’t defined in var.apps or local.default_apps, because it’s allowed to have elements defined in one map that are not also present in the other map.
  • tomap, tobool, and tostring to ensure that the result always has the expected type and to generate an error if a future maintainer populates local.default_apps incorrectly.

Note that in order to comply with the type constraint you’ll still need to write out in the variable definition all of the expected attributes, but you can set the ones you don’t intend to override to null so that the coalesce call will ignore them:

apps = {
  "api-1" = {
    is_enabled  = true
    cost_center = null
  }
}

Thank you @apparentlymart this works for me.