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
时,调用不会指定这两个文件名,这样就会使得模块使用定义的默认值。