跳至主要内容

类型约束

OpenTofu 模块作者和提供程序开发者可以使用详细的类型约束来验证用户为其输入变量和资源参数提供的值。这需要一些关于 OpenTofu 类型系统的额外知识,但允许您为您的模块和资源构建更强大的用户界面。

类型关键字和构造函数

类型约束使用类型关键字和类似函数的构造称为类型构造函数的混合来表示。

  • 类型关键字是表示静态类型的未加引号的符号。
  • 类型构造函数是后跟一对括号的未加引号的符号,括号中包含指定有关类型的更多信息的参数。如果没有其参数,类型构造函数不会完全表示一个类型;相反,它表示一种类似类型

类型约束看起来与其他类型的 OpenTofu 表达式 相似,但它们是一种特殊的语法。在 OpenTofu 语言中,它们仅在 输入变量type 参数中有效。

原始类型

原始类型是一种简单的类型,它不是由任何其他类型构成的。OpenTofu 中的所有原始类型都由类型关键字表示。可用的原始类型是

  • string:表示某些文本的 Unicode 字符序列,例如 "hello"
  • number:一个数值。number 类型可以表示诸如 15 之类的整数和诸如 6.283185 之类的分数。
  • booltruefalse 之一。bool 值可用于条件逻辑。

原始类型的转换

OpenTofu 语言将在需要时自动将 numberbool 值转换为 string 值,反之亦然,只要字符串包含数字或布尔值的有效表示。

  • true 转换为 "true",反之亦然
  • false 转换为 "false",反之亦然
  • 15 转换为 "15",反之亦然

复杂类型

复杂类型是一种将多个值组合成一个值的类型。复杂类型由类型构造函数表示,但其中一些也有简写关键字版本。

复杂类型有两类:集合类型(用于对相似值进行分组)和结构类型(用于对可能不同的值进行分组)。

集合类型

集合类型允许将一种其他类型的多个值组合在一起作为一个值。集合中值的类型称为其元素类型。所有集合类型都必须具有一个元素类型,该类型作为其构造函数的参数提供。

例如,类型 list(string) 表示“字符串列表”,它与 list(number)(数字列表)不同。集合中的所有元素都必须始终是相同类型。

OpenTofu 语言中的三种集合类型是

  • list(...):由从零开始的连续整数标识的值序列。

    关键字 listlist(any) 的简写,它接受任何元素类型,只要每个元素都是相同类型即可。这是为了与旧配置保持兼容;对于新代码,我们建议使用完整形式。

  • map(...):一个值集合,其中每个值由一个字符串标签标识。

    关键字 mapmap(any) 的简写,它接受任何元素类型,只要每个元素都是相同类型即可。这是为了与旧配置保持兼容;对于新代码,我们建议使用完整形式。

    地图可以用大括号()和冒号(:)或等号(=)来创建:{ "foo": "bar", "bar": "baz" } 或者 { foo = "bar", bar = "baz" }。如果键不是以数字开头,则可以省略引号。如果键是以数字开头,则需要加引号。单行地图中,键值对之间需要加逗号。多行地图中,键值对之间加换行符即可。

  • set(...):一组唯一值,这些值没有任何辅助标识符或排序。

结构类型

结构类型允许将多种不同类型的多个值组合在一起作为一个单独的值。结构类型需要一个模式作为参数,以指定哪些类型允许用于哪些元素。

OpenTofu 语言中的两种结构类型是

  • object(...):一组命名的属性,每个属性都有自己的类型。

    对象类型的模式是 { <KEY> = <TYPE>, <KEY> = <TYPE>, ... } —— 一对花括号,包含一个以逗号分隔的 <KEY> = <TYPE> 对序列。与对象类型匹配的值必须包含所有指定的键,并且每个键的值必须与其指定的类型匹配。(具有额外键的值仍然可以与对象类型匹配,但在类型转换期间会丢弃额外的属性。)

  • tuple(...):一个元素序列,由从零开始的连续整数标识,每个元素都有自己的类型。

    元组类型的模式是 [<TYPE>, <TYPE>, ...] —— 一对方括号,包含一个以逗号分隔的类型序列。与元组类型匹配的值必须具有完全相同数量的元素(不多不少),并且每个位置的值必须与该位置指定的类型匹配。

例如:object({ name=string, age=number }) 的对象类型将匹配以下值

代码块
{
name = "John"
age = 52
}

此外,object({ id=string, cidr_block=string }) 的对象类型将与 aws_vpc 资源引用生成的、例如 aws_vpc.example_vpc 这样的对象匹配;尽管该资源具有其他属性,但在类型转换期间会丢弃这些属性。

最后,tuple([string, number, bool]) 的元组类型将匹配以下值

代码块
["a", 15, true]

复杂类型字面量

OpenTofu 语言具有用于创建元组和对象值的字面量表达式,这些表达式在 表达式:字面量表达式 中分别描述为“列表/元组”字面量和“映射/对象”字面量。

OpenTofu 提供任何直接表示列表、映射或集合的方法。但是,由于复杂类型的自动转换(如下所述),类似复杂类型之间的差异几乎与普通用户无关,而且大多数 OpenTofu 文档混淆了列表和元组以及映射和对象。这些区别仅在限制模块或资源的输入值时才有用。

复杂类型的转换

类似的复杂类型(列表/元组/集合和映射/对象)通常可以在 OpenTofu 语言中互换使用,而且大多数 OpenTofu 文档忽略了复杂类型种类之间的差异。这是由于两种转换行为

  • 只要有可能,OpenTofu 会在提供的价值不是所请求的精确类型时,在类似的复杂类型之间转换值。“类似种类”定义如下
    • 对象和映射是相似的。
      • 如果映射(或更大的对象)具有至少对象模式所需的键,则可以将其转换为对象。任何额外的属性在转换期间都会被丢弃,这意味着映射 -> 对象 -> 映射转换可能会丢失信息。
    • 元组和列表是相似的。
      • 只有当列表具有完全所需的元素数量时,才能将其转换为元组。
    • 集合与元组和列表几乎相似
      • 当列表或元组转换为集合时,重复值将被丢弃,元素的顺序将丢失。
      • set 转换为列表或元组时,元素将以任意顺序排列。如果集合的元素是字符串,它们将按字典顺序排列;其他元素类型集合不保证元素的任何特定顺序。
  • 只要有可能,OpenTofu 会转换复杂类型中的元素值,这可以通过递归转换复杂类型的元素,或者如上面 基本类型的转换 中所述。

例如:如果模块参数需要一个 list(string) 类型的值,而用户提供了元组 ["a", 15, true],OpenTofu 将在内部将该值转换为 ["a", "15", "true"],方法是将元素转换为所需的 string 元素类型。稍后,如果模块使用这些元素来设置需要字符串、数字和布尔值的不同资源参数(分别),OpenTofu 将在此时自动将第二个和第三个字符串转换回所需的类型,因为它们包含数字和布尔值的有效表示。

另一方面,如果提供的 value(包括它的任何元素值)与所需的类型不兼容,则自动转换将失败。如果参数需要 map(string) 类型的 value,而用户提供了对象 {name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12},OpenTofu 将引发类型不匹配错误,因为元组无法转换为字符串。

动态类型: “any” 约束

关键字 any 是一个特殊的构造,用作尚未确定的类型的占位符。any 本身不是类型:在将值解释为包含 any 的类型约束时,OpenTofu 将尝试找到一个可以替换 any 关键字以产生有效结果的单个实际类型。

使用 any 的唯一情况是,如果你将给定值直接传递给其他系统,而不直接访问其内容。例如,如果你仅使用 jsonencode 将完整值直接传递给资源,则可以使用 any 类型的变量,如以下示例所示

代码块
variable "settings" {
type = any
}

resource "aws_s3_object" "example" {
# ...

# This is a reasonable use of "any" because this module
# just writes any given data to S3 as JSON, without
# inspecting it further or applying any constraints
# to its type or value.
content = jsonencode(var.settings)
}

如果模块的任何部分访问值的元素或属性,或期望它为字符串或数字,或任何其他非不透明处理,则不正确使用 any。改为编写模块期望的精确类型。

any 与集合类型

集合的所有元素必须具有相同的类型,因此,如果你将 any 用作集合的元素类型的占位符,那么 OpenTofu 将尝试找到一个要用于结果集合的单个精确元素类型。

例如,给定类型约束 list(any),OpenTofu 将检查给定值,并尝试选择一个可以使结果有效的 any 的替换。

如果给定值为 ["a", "b", "c"] —— 其物理类型为 tuple([string, string, string]) —— OpenTofu 会对此进行如下分析

  • 元组类型和列表类型根据上一节是相似的,因此元组到列表的转换规则适用。
  • 元组中的所有元素都是字符串,因此类型约束 string 对所有列表元素都是有效的。
  • 因此,在本例中,any 参数将被替换为 string,最终的具体值类型为 list(string)

如果给定元组的元素并非全部为同一类型,那么 OpenTofu 将尝试找到一个它们都可以转换到的单一类型。OpenTofu 将考虑前面几节中描述的各种转换规则。

  • 如果给定值为 ["a", 1, "b"],那么 OpenTofu 仍将选择 list(string),这是由于基本类型转换规则,由于该类型约束隐含的字符串转换,结果值将为 ["a", "1", "b"]
  • 如果给定值为 ["a", [], "b"],那么该值无法符合类型约束:没有一种单一类型,既可以将字符串转换为它,也可以将空元组转换为它。OpenTofu 将拒绝此值,并抱怨所有元素必须具有相同的类型。

虽然上面的示例使用的是 list(any),但类似的原理也适用于 map(any)set(any)

可选对象类型属性

当 OpenTofu 未收到指定对象属性的值时,通常会返回错误。当你将属性标记为可选时,OpenTofu 将改为为缺少的属性插入一个默认值。这使接收模块能够描述适当的回退行为。

要将属性标记为可选,请在对象类型约束中使用 optional 修饰符。以下示例创建了可选属性 b 和具有默认值 c 的可选属性。

代码块
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
})
}

optional 修饰符接受一个或两个参数。

  • 类型:(必填)第一个参数指定属性的类型。
  • 默认值:(可选)第二个参数定义 OpenTofu 在属性不存在时应使用的默认值。它必须与属性类型兼容。如果未指定,OpenTofu 使用相应类型的 `null` 值作为默认值。

具有非 `null` 默认值的可选属性保证在接收模块中永远不会具有 `null` 值。OpenTofu 将在调用者完全省略属性和调用者显式将其设置为 `null` 时替换默认值,从而避免了需要进行额外的检查以处理可能的 null 值。

OpenTofu 在嵌套变量类型中自上而下地应用对象属性默认值。这意味着 OpenTofu 会首先应用您在 `optional` 修饰符中指定的默认值,然后随后将任何嵌套默认值应用于该属性。

示例:具有可选属性和默认值的嵌套结构

以下示例定义了一个用于存储托管网站的存储桶的变量。此变量类型使用多个可选属性,包括 `website`,它本身是具有可选属性和默认值的可选 `object` 类型。

代码块
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)
}), {})
}))
}

以下示例 `terraform.tfvars` 文件为 `var.buckets` 指定了三个存储桶配置。

  • production 将路由规则设置为添加重定向
  • archived 使用默认配置,但已禁用
  • docs 覆盖索引文档和错误文档以使用文本文件

production 存储桶未指定索引文档和错误文档,而 `archived` 存储桶完全省略了网站配置。OpenTofu 将使用在 `bucket` 类型约束中指定的默认值。

代码块
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"
}
},
]

此配置会生成以下变量值。

  • 对于 `production` 和 `docs` 存储桶,OpenTofu 将 `enabled` 设置为 `true`。OpenTofu 还提供 `website` 的默认值,然后 `docs` 中指定的那些值会覆盖这些默认值。
  • 对于 `archived` 和 `docs` 存储桶,OpenTofu 将 `routing_rules` 设置为 `null` 值。当 OpenTofu 未收到可选属性并且没有指定的默认值时,OpenTofu 会使用 `null` 值填充这些属性。
  • 对于 `archived` 存储桶,OpenTofu 使用 `buckets` 类型约束中指定的默认值填充 `website` 属性。
代码块
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)
}
},
])

示例:有条件地设置可选属性

有时,关于是否为可选参数设置值的决定需要根据其他一些数据动态做出。在这种情况下,调用 `module` 块可以使用带 `null` 的条件表达式作为其结果分支之一,以表示动态地不设置参数。

使用上一节中显示的 `variable "buckets"` 声明,以下示例有条件地覆盖了 `website` 对象中的 `index_document` 和 `error_document` 设置,这些设置基于新的变量 `var.legacy_filenames`

代码块
variable "legacy_filenames" {
type = bool
default = false
nullable = false
}

module "buckets" {
source = "./modules/buckets"

buckets = [
{
name = "maybe_legacy"
website = {
error_document = var.legacy_filenames ? "ERROR.HTM" : null
index_document = var.legacy_filenames ? "INDEX.HTM" : null
}
},
]
}

当 `var.legacy_filenames` 设置为 `true` 时,调用将覆盖文档文件名。当它为 `false` 时,调用将不指定这两个文件名,从而允许模块使用其指定的默认值。