Thanatos
Thanatos

Reputation: 44334

How do I use a for loop to emit a list of objects in Terraform?

I have a variable defined as,

variable "ssh_permit" {
  type = list(object({ name = string, ip = string }))
  default = [
    {
      name = "alice"
      ip = "1.1.1.1"
    },
    {
      name = "bob"
      ip = "2.2.2.2"
    },
  ]
}

I'm attempting to use a for loop, to post-process this value into an object that can be assigned to the security_rule attribute on an Azure NSG.

The docs seem to indicate that this should be possible.

However, I get:

Error: Incorrect attribute value type

  on test.tf line 81, in resource "azurerm_network_security_group" "foo_nsg":
  81:   security_rule = [for idx, rule in var.ssh_permit: {
  82:       name = "allow-${rule.name}"
  83:       priority = 100 + idx
  84:       direction = "Inbound"
  85:       access = "Allow"
  86:       protocol = "TCP"
  87:       source_address_prefix = rule.ip
  88:       source_port_range = "*"
  89:       destination_address_prefix = "*"
  90:       destination_port_range = "22"
  91:     } ]
    |----------------
    | var.ssh_permit is list of object with 2 elements

Inappropriate value for attribute "security_rule": element 0: attributes
"description", "destination_address_prefixes",
"destination_application_security_group_ids", "destination_port_ranges",
"source_address_prefixes", "source_application_security_group_ids", and
"source_port_ranges" are required.

It is like Terraform is ignoring the fact that there's a for loop there. Yes, var.ssh_permit is a list of object w/ 2 elements, but it gets transformed into the appropriate object for security_element.

(I've gotten this to work with the dynamic block syntax, but I'd like to ignore that for the purposes of the question, since I'm trying to also learn the TF syntax for for loops. It appears that it should work here, so I'd like to understand why not.)

Upvotes: 0

Views: 1721

Answers (2)

Martin Atkins
Martin Atkins

Reputation: 74694

This failure is reporting that the first element value you've provided for security_rule doesn't meet the type constraint for this argument.

Based on the error message, it seems like this argument requires a list of objects with various attributes, including all of the ones listed in the error message. One way to make this result conform to the type, then, would be to set all of those extra attributes to null so that the object has a compatible type but still leaves those attributes unpopulated:

  security_rule = [
    for idx, rule in var.ssh_permit: {
      name                                       = "allow-${rule.name}"
      priority                                   = 100 + idx
      direction                                  = "Inbound"
      access                                     = "Allow"
      protocol                                   = "TCP"
      source_address_prefix                      = rule.ip
      source_port_range                          = "*"
      destination_address_prefix                 = "*"
      destination_port_range                     = "22"
      description                                = null
      destination_address_prefixes               = null
      destination_application_security_group_ids = null
      destination_port_ranges                    = null
      source_address_prefixes                    = null
      source_application_security_group_ids      = null
      source_port_ranges                         = null
    }
  ]

The above expression should make the result have a suitable type, although the provider is free to impose additional validation constraints beyond type checking and so it may not consider null to be a suitable value for all of these attributes.


You mentioned dynamic blocks and so I expect you have an underlying question here about why you don't typically need to set arguments to null in blocks but yet you do here in this expression.

This is because a block in the Terraform language is a special language structure rather than a normal value, and so it has some extra capabilities that differentiate it from values, including the idea that particular arguments can be optional, and that a block can contain nested blocks. Nested blocks are a language construct somewhat unique to Terraform, due to its intent to use a declarative style that reads like a description of a desired result rather than like a typical computer program.

However, whenever you see the security_rule = syntax you are assigning a value to an argument. Values in Terraform are a lot more like the typical idea of values in general-purpose programming languages, and in particular each value has a type. By using the { ... } syntax here, you've constructed an object-typed value. Notice that "object" isn't actually a type itself, but rather it's a subcategory of types that have attributes, similar to how in some general-purpose programming languages "object" just means an instance of any class, and the class itself is the type.

A less-conventional characteristic of the Terraform language, though, is that it has a structural type system, which means that object types are defined entirely by their shape (what attributes they have) and not by a "class name", as you might see in other languages. Resource arguments have type constraints, which state which types of values are allowed to be assigned there.

The definition of this security_rule argument in the provider seems to declare it as if it had the following type constraint (although provider-defined type constraints are not visible directly in the language itself):

list(object({
  name                                       = string
  priority                                   = number
  direction                                  = string
  access                                     = string
  protocol                                   = string
  source_address_prefix                      = string
  source_port_range                          = string
  destination_address_prefix                 = string
  destination_port_range                     = string
  description                                = string
  destination_address_prefixes               = list(string)
  destination_application_security_group_ids = list(string)
  destination_port_ranges                    = list(string)
  source_address_prefixes                    = list(string)
  source_application_security_group_ids      = list(string)
  source_port_ranges                         = list(string)
}))

(I might not have got this exactly right; I just guessed based on what we've discussed so far in this question, rather than referring to the documentation.)

Terraform decides if an object type meets an object type constraint by checking that it has at least the attributes defined in the type constraint. Type checking failed in your case because the object you provided was lacking some of the attributes from the type constraint.


I think there's an extra detail worth mentioning here for this security_rule argument in particular. Although the provider developers seem to have forgotten to document it as such, I think this argument is marked in the provider schema as using the legacy Attributes as Blocks mode, which is a special shim that Terraform supports to allow backward compatibility with some situations where provider designs were relying on configuration validation bugs in Terraform v0.11 that have since been fixed.

I won't include all of the special cases about Attributes as Blocks mode here because they only apply to some special old provider designs and they're already described in detail in the documentation page, but I wanted to mention it since I think this argument using that mode contributes to its behavior being a little confusing/inconsistent when compared to a typical resource type argument.

Upvotes: 1

Marcin
Marcin

Reputation: 238877

security_rule is a block, not an attribute. Thus you can't treat is as an attribute using

security_rule = 

It must be defined through dynamic blocks, which as you noted, works.

Your second link to example = is for attribute, not block.

Upvotes: 0

Related Questions