1.4.1.1. 类型
表达式的结果是一个值。所有的值都有一个类型,这个类型决定了这个值可以在哪里使用以及可以对它应用哪些转换。
Terraform 的某些类型之间存在隐式类型转换规则,如果无法隐式转换类型,那么不同类型数据间的赋值将会报错。
Terraform 类型分为原始类型、复杂类型,以及 null。
1.4.1.1.1. 原始类型
原始类型分三类:string、number、bool。
string代表一组 Unicode 字符串,例如:"hello"。number代表数字,可以为整数,也可以为小数。bool代表布尔值,要么为true,要么为false。bool值可以被用做逻辑判断。
number 和 bool 都可以和 string 进行隐式转换,当我们把 number 或 bool 类型的值赋给 string 类型的值,或是反过来时,Terraform 会自动替我们转换类型,其中:
true值会被转换为"true",反之亦然false值会被转换为"false",反之亦然15会被转换为"15",3.1415会被转换为"3.1415",反之亦然
1.4.1.1.2. 复杂类型
复杂类型是一组值所组成的符合类型,有两类复杂类型。
一种是集合类型。一个集合包含了一组同一类型的值。集合内元素的类型成为元素类型。一个集合变量在构造时必须确定集合类型。集合内所有元素的类型必须相同。
Terraform 支持三种集合:
list(...):列表是一组值的连续集合,可以用下标访问内部元素,下标从0开始。例如名为l的list,l[0]就是第一个元素。list类型的声明可以是list(number)、list(string)、list(bool)等,括号中的类型即为元素类型。map(...):字典类型(或者叫映射类型),代表一组键唯一的键值对,键类型必须是string,值类型任意。map(number)代表键为string类型而值为number类型,其余类推。map值有两种声明方式,一种是类似{"foo": "bar", "bar": "baz"},另一种是{foo="bar", bar="baz"}。键可以不用双引号,但如果键是以数字开头则例外。多对键值对之间要用逗号分隔,也可以用换行符分隔。推荐使用=号(Terraform 代码规范中规定按等号对齐,使用等号会使得代码在格式化后更加美观)set(...):集合类型,代表一组不重复的值。
以上集合类型都支持通配类型缩写,例如 list 等价于 list(any),map 等价于 map(any),set 等价于 set(any)。any 代表支持任意的元素类型,前提是所有元素都是一个类型。例如,将 list(number) 赋给 list(any) 是合法的,list(string) 赋给 list(any) 也是合法的,但是 list 内部所有的元素必须是同一种类型的。
第二种复杂类型是结构化类型。一个结构化类型允许多个不同类型的值组成一个类型。结构化类型需要提供一个 schema 结构信息作为参数来指明元素的结构。
Terraform 支持两种结构化类型:
object(...):对象是指一组由具有名称和类型的属性所构成的符合类型,它的 schema 信息由{ \<KEY\>=\<TYPE\>, \<KEY\>=\<TYPE\>,...}的形式描述,例如object({age=number, name=string}),代表由名为"age“类型为number,以及名为"name"类型为string两个属性组成的对象。赋给object类型的合法值必须含有所有属性值,但是可以拥有多余的属性(多余的属性在赋值时会被抛弃)。例如对于object({age=number,name=string})来说,{ age=18 }是一个非法值,而{ age=18, name="john", gender="male" }是一个合法值,但赋值时gender会被抛弃tuple(...):元组类似list,也是一组值的连续集合,但每个元素都有独立的类型。元组同list一样,也可以用下标访问内部元素,下标从0开始。元组 schema 用[\<TYPE\>, \<TYPE\>, ...]的形式描述。元组的元素数量必须与 schema 声明的类型数量相等,并且每个元素的类型必须与元组 schema 相应位置的类型相等。例如,tuple([string, number, bool])类型的一个合法值可以是["a", 15, true]
复杂类型也支持隐式类型转换。
Terraform 会尝试转换相似的类型,转换规则有:
object和map:如果一个map的键集合含有object规定的所有属性,那么map可以被转换为object,map里多余的键值对会被抛弃。由map->object->map的转换可能会丢失数据。tuple和list:当一个list元素的数量正好等于一个tuple声明的长度时,list可以被转换为tuple。例如:值为["18", "true", "john"]的list转换为tuple([number,bool, string])的结果为[18, true, "john"]set和tuple:当一个list或是tuple被转换为一个set,那么重复的值将被丢弃,并且值原有的顺序也将丢失。如果一个set被转换到list或是tuple,那么元素将按照以下顺序排列:如果set的元素是string,那么将按照字段顺序排列;其他类型的元素不承诺任何特定的排列顺序。
复杂类型转换时,元素类型将在可能的情况下发生隐式转换,类似上述 list 到 tuple 转换举的例子。
如果类型不匹配,Terraform 会报错,例如我们试图把object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12})转换到 map(string) 类型,这是不合法的,因为 name 的值为 list,无法转换为 string。
1.4.1.1.3. any
any 是 Terraform 中非常特殊的一种类型约束,它本身并非一个类型,而只是一个占位符。每当一个值被赋予一个由 any 约束的复杂类型时,Terraform 会尝试计算出一个最精确的类型来取代 any。
例如我们把 ["a", "b", "c"] 赋给 list(any),它在 Terraform 中实际的物理类型首先被编译成 tuple([string, string, string]),然后 Terraform 认为 tuple 和 list 相似,所以会尝试将它转换为 list(string)。然后 Terraform 发现 list(string) 符合 list(any) 的约束,所以会用 string 取代 any,于是赋值后最终的类型是 list(string)。
由于即使是 list(any),所有元素的类型也必须是一样的,所以某些类型转换到 list(any) 时会对元素进行隐式类型转换。例如将 ["a", 1, "b"] 赋给 list(any),Terraform 发现 1 可以转换到 "1",所以最终的值是 ["a", "1", "b"],最终的类型会是 list(string)。再比如我们想把 ["a", \[\], "b"] 转换成 list(any),由于 Terraform 无法找到一个一个合适的目标类型使得所有元素都能成功隐式转换过去,所以 Terraform 会报错,要求所有元素都必须是同一个类型的。
声明类型时如果不想有任何的约束,那么可以用 any:
variable "no_type_constraint" {
type = any
}
这样的话,Terraform 可以将任何类型的数据赋予它。
1.4.1.1.4. null
存在一种特殊值是无类型的,那就是 null。null 代表数据缺失。如果我们把一个参数设置为 null,Terraform 会认为你忘记为它赋值。如果该参数有默认值,那么 Terraform 会使用默认值;如果没有又恰巧该参数是必填字短,Terraform 会报错。null 在条件表达式中非常有用,你可以在某项条件不满足时跳过对某参数的赋值。
1.4.1.1.5. object 的 optional 成员
自 Terraform 1.3 开始,我们可以在 object 类型定义中使用 optional 修饰属性。
在 1.3 之前,如果一个 variable 的类型为 object,那么使用时必须传入一个结构完全相符的对象。例如:
variable "an_object" {
type = object({
a = string
b = string
c = number
})
}
如果我们想传入一个对象给 var.an_object,但不准备给 b 和 c 赋值,我们必须这样:
{
a = "a"
b = null
c = null
}
传入的对象必须完全匹配类型定义的结构,哪怕我们不想对某些属性赋值。这使得我们如果想要定义一些比较复杂,属性比较多的 object 类型时会给用户在使用上造成一些麻烦。
Terraform 1.3 允许我们为一个属性添加 optional 声明,还是用上面的例子:
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
})
}
在这里我们将 b 声明为 optional,如果传入的对象没有 b,则会使用 null 作为值;c 不但声明为 optional 的,还添加了 127 作为默认值,传入的对象如果没有 c,那么会使用 127 作为它的值。
optional 修饰符有这样两个参数:
- 类型:(必填)第一个参数标明了属性的类型
- 默认值:(选填)第二个参数定义了 Terraform 在对象中没有定义该属性值时使用的默认值。默认值必须与类型参数兼容。如果没有指定默认值,Terraform 会使用
null作为默认值。
一个包含非 null 默认值的 optional 属性在模块内使用时可以确保不会读到 null 值。当用户没有设置该属性,或是显式将其设置为 null 时,Terraform 会使用默认值,所以模块内无需再次判断该属性是否为 null。
Terraform 采用自上而下的顺序来设置对象的默认值,也就是说,Terraform 会先应用 optional 修饰符中的指定的默认值,然后再为其中可能存在的内嵌对象设置默认值。
1.4.1.1.5.1. 例子:带有 optional 属性和默认值的内嵌结构
下面的例子演示了一个输入变量,用来描述一个存储了静态网站内容的存储桶。该变量的类型包含了一系列的 optional 属性,包括 website,不但其自身是 optional 的,其内部包含了数个 optional 的属性以及默认值。
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 桶完全忽略了网站配置。Terraform 会使用 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"
}
},
]
该配置会产生如下的 variable 值:
- 对
production和docs桶,Terraform 会将enabled设置为true。Terraform 会同时使用默认值配置website,然后使用docs中指定的值来覆盖默认值。 - 对
archived和docs桶,Terraform 会将routing_rules设置为null。当 Terraform 没有读取到optional的属性,并且属性上没有设置默认值时,Terraform 会将这些属性设置为null。 - 对于
archived桶,Terraform 会将website属性设置为buckets类型约束中定义的默认值。
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)
}
},
])
1.4.1.1.5.2. 例子:有条件地设置一个默认属性
有时我们需要根据其他数据的值来动态决定是否要为一个 optional 参数设置值。在这种场景下,发起调用的 module 块可以使用条件表达式搭配 null 来动态地决定是否设置该参数。
还是上一个例子中的 variable "buckets" 的例子,使用下面演示的例子可以根据新输入参数 var.legacy_filenames 的值来有条件地覆盖 website 对象中 index_document 以及 error_document 的设置:
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 时,调用会覆盖 document 的文件名。当它的值为 false 时,调用不会指定这两个文件名,这样就会使得模块使用定义的默认值。