Skip to content

Latest commit

 

History

History
418 lines (339 loc) · 16.6 KB

type-constraints.mdx

File metadata and controls

418 lines (339 loc) · 16.6 KB
page_title description
Type Constraints - Configuration Language
Learn how to use type constraints to validate user inputs to modules and resources.

Type Constraints

Terraform module authors and provider developers can use detailed type constraints to validate user-provided values for their input variables and resource arguments. This requires some additional knowledge about Terraform's type system, but allows you to build a more resilient user interface for your modules and resources.

Type Keywords and Constructors

Type constraints are expressed using a mixture of type keywords and function-like constructs called type constructors.

  • Type keywords are unquoted symbols that represent a static type.
  • Type constructors are unquoted symbols followed by a pair of parentheses, which contain an argument that specifies more information about the type. Without its argument, a type constructor does not fully represent a type; instead, it represents a kind of similar types.

Type constraints look like other kinds of Terraform expressions, but are a special syntax. Within the Terraform language, they are only valid in the type argument of an input variable.

Primitive Types

A primitive type is a simple type that isn't made from any other types. All primitive types in Terraform are represented by a type keyword. The available primitive types are:

  • string: a sequence of Unicode characters representing some text, such as "hello".
  • number: a numeric value. The number type can represent both whole numbers like 15 and fractional values such as 6.283185.
  • bool: either true or false. bool values can be used in conditional logic.

Conversion of Primitive Types

The Terraform language will automatically convert number and bool values to string values when needed, and vice-versa as long as the string contains a valid representation of a number or boolean value.

  • true converts to "true", and vice-versa
  • false converts to "false", and vice-versa
  • 15 converts to "15", and vice-versa

Complex Types

A complex type is a type that groups multiple values into a single value. Complex types are represented by type constructors, but several of them also have shorthand keyword versions.

There are two categories of complex types: collection types (for grouping similar values), and structural types (for grouping potentially dissimilar values).

Collection Types

A collection type allows multiple values of one other type to be grouped together as a single value. The type of value within a collection is called its element type. All collection types must have an element type, which is provided as the argument to their constructor.

For example, the type list(string) means "list of strings", which is a different type than list(number), a list of numbers. All elements of a collection must always be of the same type.

The three kinds of collection type in the Terraform language are:

  • list(...): a sequence of values identified by consecutive whole numbers starting with zero.

    The keyword list is a shorthand for list(any), which accepts any element type as long as every element is the same type. This is for compatibility with older configurations; for new code, we recommend using the full form.

  • map(...): a collection of values where each is identified by a string label.

    The keyword map is a shorthand for map(any), which accepts any element type as long as every element is the same type. This is for compatibility with older configurations; for new code, we recommend using the full form.

    Maps can be made with braces ({}) and colons (:) or equals signs (=): { "foo": "bar", "bar": "baz" } OR { foo = "bar", bar = "baz" }. Quotes may be omitted on keys, unless the key starts with a number, in which case quotes are required. Commas are required between key/value pairs for single line maps. A newline between key/value pairs is sufficient in multi-line maps.

    Note: although colons are valid delimiters between keys and values, they are currently ignored by terraform fmt (whereas terraform fmt will attempt vertically align equals signs).

  • set(...): a collection of unique values that do not have any secondary identifiers or ordering.

Structural Types

A structural type allows multiple values of several distinct types to be grouped together as a single value. Structural types require a schema as an argument, to specify which types are allowed for which elements.

The two kinds of structural type in the Terraform language are:

  • object(...): a collection of named attributes that each have their own type.

    The schema for object types is { <KEY> = <TYPE>, <KEY> = <TYPE>, ... } — a pair of curly braces containing a comma-separated series of <KEY> = <TYPE> pairs. Values that match the object type must contain all of the specified keys, and the value for each key must match its specified type. (Values with additional keys can still match an object type, but the extra attributes are discarded during type conversion.)

  • tuple(...): a sequence of elements identified by consecutive whole numbers starting with zero, where each element has its own type.

    The schema for tuple types is [<TYPE>, <TYPE>, ...] — a pair of square brackets containing a comma-separated series of types. Values that match the tuple type must have exactly the same number of elements (no more and no fewer), and the value in each position must match the specified type for that position.

For example: an object type of object({ name=string, age=number }) would match a value like the following:

{
  name = "John"
  age  = 52
}

Also, an object type of object({ id=string, cidr_block=string }) would match the object produced by a reference to an aws_vpc resource, like aws_vpc.example_vpc; although the resource has additional attributes, they would be discarded during type conversion.

Finally, a tuple type of tuple([string, number, bool]) would match a value like the following:

["a", 15, true]

Complex Type Literals

The Terraform language has literal expressions for creating tuple and object values, which are described in Expressions: Literal Expressions as "list/tuple" literals and "map/object" literals, respectively.

Terraform does not provide any way to directly represent lists, maps, or sets. However, due to the automatic conversion of complex types (described below), the difference between similar complex types is almost never relevant to a normal user, and most of the Terraform documentation conflates lists with tuples and maps with objects. The distinctions are only useful when restricting input values for a module or resource.

Conversion of Complex Types

Similar kinds of complex types (list/tuple/set and map/object) can usually be used interchangeably within the Terraform language, and most of Terraform's documentation glosses over the differences between the kinds of complex type. This is due to two conversion behaviors:

  • Whenever possible, Terraform converts values between similar kinds of complex types if the provided value is not the exact type requested. "Similar kinds" is defined as follows:
    • Objects and maps are similar.
      • A map (or a larger object) can be converted to an object if it has at least the keys required by the object schema. Any additional attributes are discarded during conversion, which means map -> object -> map conversions can be lossy.
    • Tuples and lists are similar.
      • A list can only be converted to a tuple if it has exactly the required number of elements.
    • Sets are almost similar to both tuples and lists:
      • When a list or tuple is converted to a set, duplicate values are discarded and the ordering of elements is lost.
      • When a set is converted to a list or tuple, the elements will be in an arbitrary order. If the set's elements were strings, they will be in lexicographical order; sets of other element types do not guarantee any particular order of elements.
  • Whenever possible, Terraform converts element values within a complex type, either by converting complex-typed elements recursively or as described above in Conversion of Primitive Types.

For example: if a module argument requires a value of type list(string) and a user provides the tuple ["a", 15, true], Terraform will internally transform the value to ["a", "15", "true"] by converting the elements to the required string element type. Later, if the module uses those elements to set different resource arguments that require a string, a number, and a bool (respectively), Terraform will automatically convert the second and third strings back to the required types at that time, since they contain valid representations of a number and a bool.

On the other hand, automatic conversion will fail if the provided value (including any of its element values) is incompatible with the required type. If an argument requires a type of map(string) and a user provides the object {name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}, Terraform will raise a type mismatch error, since a tuple cannot be converted to a string.

Dynamic Types: The "any" Constraint

The keyword any is a special construct that serves as a placeholder for a type yet to be decided. any is not itself a type: when interpreting a value against a type constraint containing any, Terraform will attempt to find a single actual type that could replace the any keyword to produce a valid result.

For example, given the type constraint list(any), Terraform will examine the given value and try to choose a replacement for the any that would make the result valid.

If the given value were ["a", "b", "c"] -- whose physical type is tuple([string, string, string]), Terraform analyzes this as follows:

  • Tuple types and list types are similar per the previous section, so the tuple-to-list conversion rule applies.
  • All of the elements in the tuple are strings, so the type constraint string would be valid for all of the list elements.
  • Therefore in this case the any argument is replaced with string, and the final concrete value type is list(string).

All of the elements of a collection must have the same type, so conversion to list(any) requires that all of the given elements must be convertible to a common type. This implies some other behaviors that result from the conversion rules described in earlier sections.

  • If the given value were instead ["a", 1, "b"] then Terraform would still select list(string), because of the primitive type conversion rules, and the resulting value would be ["a", "1", "b"] due to the string conversion implied by that type constraint.
  • If the given value were instead ["a", [], "b"] then the value cannot conform to the type constraint: there is no single type that both a string and an empty tuple can convert to. Terraform would reject this value, complaining that all elements must have the same type.

Although the above examples use list(any), a similar principle applies to map(any) and set(any).

If you wish to apply absolutely no constraint to the given value, the any keyword can be used in isolation:

variable "no_type_constraint" {
  type = any
}

In this case, Terraform will replace any with the exact type of the given value and thus perform no type conversion whatsoever.

Optional Object Type Attributes

Terraform v1.3 adds support for marking particular attributes as optional in an object type constraint.

To mark an attribute as optional, use the additional optional(...) modifier around its type declaration:

variable "with_optional_attribute" {
  type = object({
    a = string                # a required attribute
    b = optional(string)      # an optional attribute
    c = optional(number, 127) # an optional attribute with default value
  })
}

When evaluating variable values, Terraform will return an error if an object attribute specified in the variable type is not present in the given value. Marking an attribute as optional changes the behavior in that situation: Terraform will instead insert a default value for the missing attribute, allowing the receiving module to describe an appropriate fallback behavior.

The optional modifier takes one or two arguments. The first argument specifies the type of the attribute, and (if given) the second attribute defines the default value to use if the attribute is not present. The default must be compatible with the attribute type. If no default is specified, a null value of the appropriate type will be used as the default.

During evaluation, object attribute defaults are applied top-down in nested variable types. This means that a given attribute's default value will also have any nested default values applied to it later.

Example: Nested Structures with Optional Attributes and Defaults

The following configuration defines a variable which describes a number of storage buckets, each of which is used to host a website. This variable type uses several optional attributes, one of which is itself an object type with optional attributes and defaults.

terraform {
  # Optional attributes are currently experimental.
  experiments = [module_variable_optional_attrs]
}

variable "buckets" {
  type = list(object({
    name    = string
    enabled = optional(bool, true)
    website = optional(object({
      index_document = optional(string, "index.html")
      error_document = optional(string, "error.html")
      routing_rules  = optional(string)
    }), {})
  }))
}

To test this out, we can create a file terraform.tfvars to provide an example value for var.buckets:

buckets = [
  {
    name = "production"
    website = {
      routing_rules = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]
      EOT
    }
  },
  {
    name = "archived"
    enabled = false
  },
  {
    name = "docs"
    website = {
      index_document = "index.txt"
      error_document = "error.txt"
    }
  },
]

The intent here is to specify three bucket configurations:

  • production sets the routing rules to add a redirect;
  • archived uses default configuration but is disabled;
  • docs overrides the index and error documents to use text files.

Note that production does not specify the index and error documents, and archived omits the website configuration altogether. Because our type specifies a default value for the website attribute as an empty object {}, Terraform fills in the defaults specified in the nested type.

The resulting variable value is:

tolist([
  {
    "enabled" = true
    "name" = "production"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = <<-EOT
      [
        {
          "Condition" = { "KeyPrefixEquals": "img/" },
          "Redirect"  = { "ReplaceKeyPrefixWith": "images/" }
        }
      ]

      EOT
    }
  },
  {
    "enabled" = false
    "name" = "archived"
    "website" = {
      "error_document" = "error.html"
      "index_document" = "index.html"
      "routing_rules" = tostring(null)
    }
  },
  {
    "enabled" = true
    "name" = "docs"
    "website" = {
      "error_document" = "error.txt"
      "index_document" = "index.txt"
      "routing_rules" = tostring(null)
    }
  },
])

Here we can see that for production and docs, the enabled attribute has been filled in as true. The default values for the website attribute have also been filled in, with the values specified by docs overriding the defaults. For archived, the entire default website value is populated.

One important point is that the website attribute for the archived and docs buckets contains a null value for routing_rules. When declaring a type constraint with an optional object attributes without a default, a value which omits that attribute will be populated with a null value, rather than continuing to omit the attribute in the final result.

Experimental Status

Because this feature is currently experimental, it requires an explicit opt-in on a per-module basis. To use it, write a terraform block with the experiments argument set as follows:

terraform {
  experiments = [module_variable_optional_attrs]
}

Until the experiment is concluded, the behavior of this feature may see breaking changes even in minor releases. We recommend using this feature only in prerelease versions of modules as long as it remains experimental.