Inappropriate value for attribute "name": string required

Am trying to install multiple extensions using a variable as a list but getting the “Error: Incorrect attribute value type”. Any suggestion?

variable "pg_extensions" {
  type    = list(string)
  default = ["pglogical", "pg_trgm"]
}

resource "postgresql_extension" "create_extension" {
  name     =  var.pg_extensions
  database =  "test"  
}

Error:

Error: Incorrect attribute value type

  on db.tf line 54, in resource "postgresql_extension" "create_extension":
  54:   name     =  var.pg_extensions

Inappropriate value for attribute "name": string required.

Hi @gowthamakanthan,

According to the error message, this name argument requires only a single string, and so indeed you can’t assign a list of strings to it.

Assuming this postgresql_extension resource type is the one belonging to the cyrilgdn/postgresql provider, it seems like this resource type is designed to register only one extension at a time, and so to register more than one you will need to either declare multiple resource blocks (one for each extension) or use the for_each meta-argument to declare multiple resource instances from this single resource block.

Since you’ve shown the extension names arriving in a variable I’m going to assume you’ll want to take the second option, so that the number and names of instances can be decided dynamically based on that variable.

It seems like PostgreSQL extensions are not registered in any meaningful order and so the first thing I would change here is to specify the different type constraint set(string) for your variable, which declares that it’s an unordered collection of unique strings:

variable "pg_extensions" {
  type    = set(string)
  default = ["pglogical", "pg_trgm"]
}

Along with being a more accurate description of the remote system’s model, this also means that the value is naturally compatible with for_each without any additional conversions or transformations. You can refer to this variable directly in the for_each meta-argument, like this:

resource "postgresql_extension" "all" {
  for_each = var.pg_extensions

  name     = each.value
  database = "test"  
}

When you use a set(string) as the for_each, Terraform understands that as a shorthand for specifying a map from string to string where the keys and values are identical. Therefore each.key and each.value will both produce the same result in this block, and so I just arbitrarily chose each.value here.

The keys for for_each (as reflected in each.key) are important because they also tell Terraform how to build a unique resource address to track each object in the Terraform state. With the above resource block and the default value for the variable, this will declare the following two instances:

  • postgresql_extension.all["pglogical"]
  • postgresql_extension.all["pg_trgm"]

In this particular case the tracking keys are probably not super important, but carefully selecting an appropriate unique key for each instance is typically important to enable ongoing maintenance of your configuration: if you add a new entry to var.pg_extensions later then Terraform will understand that as adding a new instance of that resource. If you were to rename one of them then, since Terraform is using those names as the unique keys, Terraform would understand it as destroying the old name and creating the new name.

If you use the for_each meta argument in other situations in future, you may find situations where updating an object in-place is a meaningful operation. In that case, a typical approach is to use a map from string keys to values, and then use the values to populate the configuration. That way if you change the values without changing the keys, Terraform can understand that your intention is to update an existing object rather than to create a new one or destroy an existing one.

Thanks for your quick and detailed response. I need to have another for_each statement to install the extensions on all of the available replicas. is there a way to use multiple for statement, something like below?

resource "postgresql_extension" "create_extension" {
  for_each =  toset([for db in lookup(var.db_replicas, local.aws_account) : lower(db)])
  for_each = var.pg_extensions
  name     =  each.value
  database =  "test"  
}

Hi @gowthamakanthan,

Assuming that “replica” means a different value for the database argument, a typical approach to a problem like this would be to construct a data structure which has one element for each pair of database and extension name that you want to declare.

If you want to produce an exhaustive set of combinations of all of the database names and all of the extension names then from a set-theory perspective that’s called the cartesian product of the two sets, which in Terraform is implemented by the setproduct function. You can use that function in combination with a for expression to build a set of objects where each object represents one pairing of database and extension name.

locals {
  database_extensions = toset([
    for pair in setproduct(var.db_replicas[local.aws_account], var.pg_extensions) : {
      database  = pair[0]
      extension = pair[1]
    }
  ])
}

The result of this is a set of objects which will look something like this (using some example names for the “replicas”):

toset([
  { database = "replica1", extension = "pglogical" }
  { database = "replica1", extension = "pg_trgm" }
  { database = "replica2", extension = "pglogical" }
  { database = "replica2", extension = "pg_trgm" }
])

This collection now meets one of the two main requirements for for_each: there’s one element per instance you want to declare. The second requirement is that the value be a mapping whose element keys will become the instance keys, which means that we need to give each of the elements of the set a unique key. These elements don’t have any explicit unique key, but we know that the pair of database and extension should be unique across all of them and so we can concatenate those together to produce a key, using another for expression:

resource "postgresql_extension" "all" {
  for_each = {
    for de in local.database_extensions :
    "${de.name}:${de.extension}" => de
  }

  name     = each.value.extension
  database = each.value.database
}

When I’m converting a set to a map purely to create unique keys for for_each I typically prefer to perform that final conversion inline in the for_each expression, rather than declaring another local value, but you can declare it as a named local value and refer to that value in for_each instead if you prefer; the effect is the same either way.

Notice that unlike in my first answer for_each now really is a map. Previously I noted that Terraform will treat a set of strings here as if it were a map from those strings to themselves, but now we have a real map the distinction between each.key and each.value becomes significant: each.value is the current object, and so each.value.extension and each.value.database refer to the current instance’s extension name and database name respectively.

Due to the key concatenation scheme I used here, this would declare four instances of the resource with the following addresses, if we assume the same placeholder replica names I used for the set example above:

  • postgresql_extension.all["replica1:pglogical"]
  • postgresql_extension.all["replica1:pg_trgm"]
  • postgresql_extension.all["replica2:pglogical"]
  • postgresql_extension.all["replica2:pg_trgm"]

This addressing scheme will allow Terraform to understand the meaning of future changes to either var.pg_extensions or var.db_replicas[local.aws_account]. For example, if you were to remove pg_trgm from the set of extensions then Terraform would see that both postgresql_extension.all["replica1:pg_trgm"] and postgresql_extension.all["replica2:pg_trgm"] are no longer declared, but the two with pglogical would remain unchanged because their compound keys would still be present in the map.

1 Like

A post was split to a new topic: Offer a choice when applying a Terraform configuration