1.4.8.1. 表达式
表达式用来在配置文件中进行一些计算。最简单的表达式就是字面量,比如 "hello"
,或者 5
。Terraform
也支持一些更加复杂的表达式,比如引用其他 resource
的输出值、数学计算、布尔条件计算,以及一些内建的函数。
Terraform 配置中很多地方都可以使用表达式,但某些特定的场景下限制了可以使用的表达式的类型,例如只准使用特定数据类型的字面量,或是禁止使用 resource
的输出值。
您可以通过运行 terraform console
命令,从 Terraform 表达式控制台测试 Terraform 表达式的行为。
我们在类型章节中已经基本介绍了类型以及类型相关的字面量,下面我们来介绍一些其他的表达式。
1.4.8.1.1. 下标和属性
list
和 tuple
可以通过下标访问成员,例如 local.list[3]
、var.tuple[2]
。map
和 object
可以通过属性访问成员,例如 local.object.attrname
、local.map.keyname
。由于 map
的键是用户定义的,可能无法成为合法的 Terraform 标识符,所以访问 map
成员时我们推荐使用方括号:local.map["keyname"]
。
1.4.8.1.2. 引用命名值
Terraform 中定义了多种命名值,表达式中的每一个命名值都关联到一个具体的值,我们可以用单一命名值作为一个表达式,或是组合多个命名值来计算出一个新值。
命名值有如下种类:
<RESOURCE TYPE>.<NAME>
:表示一个资源对象。凡是不符合后面列出的命名值模式的表达式都会被 Terraform 解释为一个托管资源。如果资源声明了count
元参数,那么该表达式表示的是一个对象实例的list
。如果资源声明了for_each
元参数,那么该表达式表示的是一个对象实例的map
。var.<NAME>
:表示一个输入变量local.<NAME>
:表示一个局部值module.<MODULE_NAME>.<OUTPUT_NAME>
:表示一个模块的一个输出值data.<DATA_TYPE>.<NAME>
:表示一个数据源实例。如果数据源声明了count
元参数,那么该表达式表示的是一个数据源实例list
。如果数据源声明了for_each
元参数,那么该表达式表示的是一个数据源实例map
。path.module
:表示当前模块在文件系统中的路径path.root
:表示根模块(调用 Terraform 命令行执行的代码文件所在的模块)在文件系统中的路径path.cwd
:表示当前工作目录的路径。一般来说该路径等同于path.root
,但在调用 Terraform 命令行时如果指定了代码路径,那么二者将会不同。terraform.workspace
:当前使用的 Workspace (我们在状态管理的"状态的隔离存储"中介绍过)
虽然这些命名表达式可以使用 .<NAME>
号来访问对象的各种属性,但实际上他们实际类型并不是我们在类型章节里提到过的 object
。两者的区别在于,object
同时支持使用 .<NAME>
或者 ["<NAME>"]
两种方式访问对象成员属性,而上述命名表达式仅支持 .<NAME>
。
1.4.8.1.3. 局部命名值
在某些特定表达式或上下文当中,有一些特殊的命名值可以被使用,他们是局部命名值。几种比较常见的局部命名值有:
count.index
:表达当前count
下标序号each.key
:表达当前for_each
迭代器实例self
:在预置器中指代声明预置器的资源
1.4.8.1.4. 命名值的依赖关系
构建资源或是模块时经常会使用含有命名值的表达式赋值,Terraform 会分析这些表达式并自动计算出对象之间的依赖关系。
1.4.8.1.5. 引用资源输出属性
最常见的引用类型就是引用一个 resource
或 data
块定义的对象的输出属性。由于这些资源与数据源对象结构可能非常复杂,所以对它们的输出属性的引用表达式也可能非常复杂。
比如下面这个例子:
resource "aws_instance" "example" {
ami = "ami-abc123"
instance_type = "t2.micro"
ebs_block_device {
device_name = "sda2"
volume_size = 16
}
ebs_block_device {
device_name = "sda3"
volume_size = 20
}
}
aws_instance
文档列出了该类型所支持的所有输入参数和内嵌块,以及对外输出的属性列表。所有这些不同的资源类型 Schema 都可以在引用中使用,如下所示:
ami
参数可以在可以在其他地方用aws_instance.example.ami
表达式来引用id
属性可以用aws_instance.example.id
的表达式来引用- 内嵌的
ebs_block_device
参数可以通过后面会介绍的展开表达式(splat expression)来访问,比如我们获取所有的ebs_block_device
的device_name
列表:aws_instance.example.ebs_block_device[*].device_name
- 在
aws_instance
类型里的内嵌块并没有任何输出属性,但如果ebs_block_device
添加了一个名为"id"
的输出属性,那么可以用aws_instance.example.ebs_block_device[*].id
表达式来访问含有所有id
的列表 - 有时多个内嵌块会各自包含一个逻辑键来区分彼此,类似用资源名访问资源,我们也可以用内嵌块的名字来访问特定内嵌块。假如
aws_instance
类型有一个假想的内嵌块类型device
并规定device
可以赋予这样的一个逻辑键,那么代码看起来就会是这样的:
device "foo" {
size = 2
}
device "bar" {
size = 4
}
我们可以使用键来访问特定块的数据,例如:aws_instance.example.device["foo"].size
要获取一个 device
名称到 device
大小的映射,可以使用 for
表达式:
{for k, device in aws_instance.example.device : k => device.size}
当一个资源声明了 count
参数,那么资源本身就成了一个资源对象列表而非单个资源。这种情况下要访问资源输出属性,要么使用展开表达式,要么使用下标索引:
aws_instance.example[*].id
:返回所有 instance 的id
列表aws_instance.example[0].id
:返回第一个 instance的id
当一个资源声明了 for_each
参数,那么资源本身就成了一个资源对象字典而非单个资源。这种情况下要访问资源的输出属性,要么使用特定键,要么使用 for
表达式:
aws_instance.example["a"].id
:返回"a"
对应的实例的id
[for value in aws_instance.example: value.id]
:返回所有 instance 的id
注意不像使用 count
,使用 for_each
的资源集合不能直接使用展开表达式,展开表达式只能适用于列表。你可以把字典转换成列表后再使用展开表达式:
values(aws_instance.example)[*].id
1.4.8.1.6. 尚不知晓的值
当 Terraform 在计算变更计划时,有些资源输出属性无法立即求值,因为他们的值取决于远程API的返回值。比如说,有一个远程对象可以在创建时返回一个生成的唯一 id
,Terraform 无法在创建它之前就预知这个值。
为了允许在计算变更阶段就能计算含有这种值的表达式,Terraform 使用了一个特殊的"尚不知晓(unknown value)"占位符来代替这些结果。大部分时候你不需要特意理会它们,因为 Terraform 语言会自动处理这些尚不知晓的值,比如说使两个尚不知晓的值相加得到的会是一个尚不知晓的值。
然而,有些情况下表达式中含有尚不知晓的值会有明显的影响:
count
元参数不可以为尚不知晓,因为变更计划必须明确地知晓到底要维护多少个目标实例- 如果尚不知晓的值被用于数据源,那么数据源在计算变更计划阶段就无法读取,它会被推迟到执行阶段读取。这种情况下,在计划阶段该数据源的一切输出均为尚不知晓
- 如果声明
module
块时传递给模块输入变量的表达式使用了尚不知晓值,那么在模块代码中任何使用了该输入变量值的表达式的值都将是尚不知晓 - 如果模块输出值表达式中含有尚不知晓值,任何使用该模块输出值的表达式都将是尚不知晓
- Terraform 会尝试验证尚不知晓值的数据类型是否合法,但仍然有可能无法正确检查数据类型,导致执行阶段发生错误
尚不知晓值在执行 terraform plan
时会被输出为 "(not yet known)"。
1.4.8.1.7. 算数和逻辑操作符
一个操作符是一种用以转换或合并一个或多个表达式的表达式。操作符要么是把两个值计算为第三个值,也就是二元操作符;要么是把一个值转换成另一个值,也就是一元操作符。
二元操作符位于两个表达式的中间,类似 1+2
。一元操作符位于一个表达式的前面,类似 !true
。
Terraform 的 HCL 语言支持一组算数和逻辑操作符,它们的功能类似于 JavaScript 或 Ruby 里的操作符功能。
当一个表达式中含有多个操作符时,它们的优先级顺序为:
!
,-
(负号)*
,/
,%
+
,-
(减号)>
,>=
,<
,<=
==
,!=
&&
||
可以使用小括号覆盖默认优先级。如果没有小括号,高优先级操作符会被先计算,例如 1+2*3
会被解释成 1+(2*3)
而不是 (1+2)*3
。
不同的操作符可以按它们之间相似的行为被归纳为几组,每一组操作符都期待被给予特定类型的值。Terraform 会在类型不符时尝试进行隐式类型转换,如果失败则会抛错。
1.4.8.1.7.1. 算数操作符
a + b
:返回a
与b
的和a - b
:返回a
与b
的差a * b
:返回a
与b
的积a / b
:返回a
与b
的商a % b
:返回a
与b
的模。该操作符一般仅在a
与b
是整数时有效-a
:返回a
与-1
的商
1.4.8.1.7.2. 相等性操作符
a == b
:如果a
与b
类型与值都相等返回true
,否则返回false
a != b
:与==
相反
1.4.8.1.7.3. 比较操作符
a < b
:如果a
比b
小则为true
,否则为false
a > b
:如果a
比b
大则为true
,否则为false
a <= b
:如果a
比b
小或者相等则为true
,否则为false
a >= b
:如果a
比b
大或者相等则为true
,否则为false
1.4.8.1.7.4. 逻辑操作符
a || b
:a
或b
中有至少一个为true
则为true
,否则为false
a && b
:a
与比都为true
则为true
,否则为false
!a
:如果a
为true
则为false
,如果a
为false
则为true
1.4.8.1.8. 条件表达式
条件表达式是判断一个布尔表达式的结果以便于在后续两个值当中选择一个:
condition ? true_val : false_val
如果 condition
表达式为 true
,那么结果是 true_value
,反之则为 false_value
。
一个常见的条件表达式用法是使用默认值替代非法值:
var.a != "" ? var.a : "default-a"
(注:以上表达式目前推荐写为:coalesce(var.a, "default-a")
)
如果输入变量 a
的值是空字符串,那么结果会是 default-a
,否则返回输入变量 a
的值。
条件表达式的判断条件可以使用上述的任意操作符。供选择的两个值也可以是任意类型,但它们的类型必须相同,这样 Terraform 才能判断条件表达式的输出类型。
1.4.8.1.9. 函数调用
Terraform 支持在计算表达式时使用一些内建函数,函数调用表达式类似操作符,通用语法是:
<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
函数名标明了要调用的函数。每一个函数都定义了数量不等、类型不一的入参以及不同类型的返回值。
有些函数定义了不定长的入参表,例如,min
函数可以接收任意多个数值类型入参,返回其中最小的数值:
min(55, 3453, 2)
1.4.8.1.9.1. 展开函数入参
如果想要把列表或元组的元素作为参数传递给函数,那么我们可以使用展开符:
min([55, 2453, 2]...)
展开符使用的是三个独立的 .
号组成的 ...
,不是 Unicode 中的省略号 …
。展开符是一种只能用在函数调用场景下的特殊语法。
有关完整的内建函数我们可能会在今后撰写相应的章节介绍。
1.4.8.1.10. for 表达式
for
表达式是将一种复杂类型映射成另一种复杂类型的表达式。输入类型值中的每一个元素都会被映射为一个或零个结果。
举例来说,如果 var.list
是一个字符串列表,那么下面的表达式将会把列表元素全部转为大写:
[for s in var.list : upper(s)]
在这里 for
表达式迭代了 var.list
中每一个元素(就是 s
),然后计算了 upper(s)
,最后构建了一个包含了所有 upper(s)
结果的新元组,元组内元素顺序与源列表相同。
for
表达式周围的括号类型决定了输出值的类型。上面的例子里我们使用了方括号,所以输出类型是元组。如果使用的是花括号,那么输出类型是对象,for
表达式内部冒号后面应该使用以 =>
符号分隔的表达式:
{for s in var.list : s => upper(s)}
该表达式返回一个对象,对象的成员属性名称就是源列表中的元素,值就是对应的大写值。
一个 for
表达式还可以包含一个可选的 if
子句用以过滤结果,这可能会减少返回的元素数量:
[for s in var.list : upper(s) if s != ""]
被 for
迭代的也可以是对象或者字典,这样的话迭代器就会被表示为两个临时变量:
[for k, v in var.map : length(k) + length(v)]
最后,如果返回类型是对象(使用花括号)那么表达式中可以使用 ...
符号实现 group by:
{for s in var.list : substr(s, 0, 1) => s... if s != ""}
1.4.8.1.11. 展开表达式(Splat Expression)
展开表达式提供了一种类似 for
表达式的简洁表达方式。比如说 var.list
包含一组对象,每个对象有一个属性 id
,那么读取所有 id
的 for
表达式会是这样:
[for o in var.list : o.id]
与之等价的展开表达式是这样的:
var.list[*].id
这个特殊的 [*]
符号迭代了列表中每一个元素,然后返回了它们在 .
号右边的属性值。
展开表达式只能被用于列表(所以使用 for_each
参数的资源不能使用展开表达式,因为它的类型是字典)。然而,如果一个展开表达式被用于一个既不是列表又不是元组的值,那么这个值会被自动包装成一个单元素的列表然后被处理。
比如说,var.single_object[*].id
等价于 [var.single_object][*].id
。大部分场景下这种行为没有什么意义,但在访问一个不确定是否会定义 count
参数的资源时,这种行为很有帮助,例如:
aws_instance.example[*].id
上面的表达式不论 aws_instance.example
定义了 count
与否都会返回实例的 id
列表,这样如果我们以后为 aws_instance.example
添加了 count
参数我们也不需要修改这个表达式。
1.4.8.1.11.1. 遗留的旧有展开表达式
曾经存在另一种旧的展开表达式语法,它是一种比较弱化的展开表达式,现在应该尽量避免使用。
这种旧的展开表达式使用 .*
而不是 [*]
:
var.list.*.interfaces[0].name
要特别注意该表达式与现有的展开表达式结果不同,它的行为等价于:
[for o in var.list : o.interfaces][0].name
而现有 [*]
展开表达式的行为等价于:
[for o in var.list : o.interfaces[0].name]
注意两者右方括号的位置。
1.4.8.1.12. dynamic 块
在顶级块,例如 resource
块当中,一般只能以类似 name = expression
的形式进行一对一的赋值。大部分情况下这已经够用了,但某些资源类型包含了可重复的内嵌块,无法使用表达式循环赋值:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name" # can use expressions here
setting {
# but the "setting" block is always a literal block
}
}
你可以用 dynamic
块来动态构建重复的 setting
这样的内嵌块:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
application = "${aws_elastic_beanstalk_application.tftest.name}"
solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"
dynamic "setting" {
for_each = var.settings
content {
namespace = setting.value["namespace"]
name = setting.value["name"]
value = setting.value["value"]
}
}
}
dynamic
可以在 resource
、data
、provider
和 provisioner
块内使用。一个 dynamic
块类似于 for
表达式,只不过它产生的是内嵌块。它可以迭代一个复杂类型数据然后为每一个元素生成相应的内嵌块。在上面的例子里:
dynamic
的标签(也就是"setting"
)确定了我们要生成的内嵌块种类for_each
参数提供了需要迭代的复杂类型值iterator
参数(可选)设置了用以表示当前迭代元素的临时变量名。如果没有设置iterator
,那么临时变量名默认就是dynamic
块的标签(也就是setting
)labels
参数(可选)是一个表示块标签的有序列表,用以按次序生成一组内嵌块。有labels
参数的表达式里可以使用临时的iterator
变量- 内嵌的
content
块定义了要生成的内嵌块的块体。你可以在content
块内部使用临时的iterator
变量
由于 for_each
参数可以是集合或者结构化类型,所以你可以使用 for
表达式或是展开表达式来转换一个现有集合的类型。
iterator
变量(上面的例子里就是 setting
)有两个属性:
key
:迭代容器如果是map
,那么就是当前元素的键;迭代容器如果是list
,那么就是当前元素在list
中的下标序号;如果是由for_each
表达式产出的set
,那么key
和value
是一样的,这时我们不应该使用key
。value
:当前元素的值
一个 dynamic
块只能生成属于当前块定义过的内嵌块参数。无法生成诸如 lifecycle
、provisioner
这样的元参数,因为 Terraform 必须在确保对这些元参数求值的计算是成功的。
for_each
的值必须是不为空的 map
或者 set
。如果你需要根据内嵌数据结构或者多个数据结构的元素组合来声明资源实例集合,你可以使用 Terraform 表达式和函数来生成合适的值。
1.4.8.1.12.1. dynamic 块的最佳实践
过度使用 dynamic
块会导致代码难以阅读以及维护,所以我们建议只在需要构造可重用的模块代码时使用 dynamic
块。尽可能手写内嵌块。
1.4.8.1.13. 字符串字面量
Terraform 有两种不同的字符串字面量。最通用的就是用一对双引号包裹的字符,比如 "hello"
。在双引号之间,反斜杠 \
被用来进行转义。Terraform 支持的转义符有:
Sequence | Replacement |
---|---|
\n |
换行 |
\r |
回车 |
\t |
制表符 |
\" |
双引号 (不会截断字符串) |
\\ |
反斜杠 |
\uNNNN |
普通字符映射平面的Unicode字符(NNNN代表四位16进制数) |
\UNNNNNNNN |
补充字符映射平面的Unicode字符(NNNNNNNN代表八位16进制数) |
另一种字符串表达式被称为 "heredoc" 风格,是受 Unix Shell 语言启发。它可以使用自定义的分隔符更加清晰地表达多行字符串:
<<EOT
hello
world
EOT
<<
标记后面直到行尾组成的标识符开启了字符串,然后 Terraform 会把剩下的行都添加进字符串,直到遇到与标识符完全相等的字符串为止。在上面的例子里,EOT
就是标识符。任何字符都可以用作标识符,但传统上标识符一般以 EO
开头。上面例子里的 EOT
代表"文本的结束(end of text)"。
上面例子里的 heredoc 风格字符串要求内容必须对齐行头,这在块内声明时看起来会比较奇怪:
block {
value = <<EOT
hello
world
EOT
}
为了改进可读性,Terraform 也支持缩进的 heredoc,只要把 <<
改成 <<-
:
block {
value = <<-EOT
hello
world
EOT
}
上面的例子里,Terraform 会以最靠近行头的行作为基准来调整行头缩进,得到的字符串是这样的:
hello
world
heredoc 中的反斜杠不会被解释成转义,而只会是简单的反斜杠。
双引号和 heredoc 两种字符串都支持字符串模版,模版的形式是 ${...}
以及 %{...}
。如果想要表达 ${
或者 %{
的字面量,那么可以重复第一个字符:$${
和 %%{
。
1.4.8.1.14. 字符串模版
字符串模版允许我们在字符串中嵌入表达式,或是通过其他值动态构造字符串。
1.4.8.1.14.1. 插值(Interpolation)
一个 ${...}
序列被称为插值,插值计算花括号之间的表达式的值,有必要的话将之转换为字符串,然后插入字符串模版,形成最终的字符串:
"Hello, ${var.name}!"
上面的例子里,输入变量 var.name
的值被访问后插入了字符串模版,产生了最终的结果,比如:"Hello, Juan!"
1.4.8.1.14.2. 命令(Directive)
一个 %{...}
序列被称为命令,命令可以是一个布尔表达式或者是对集合的迭代,类似条件表达式以及 for
表达式。有两种命令:
if \<BOOL\>
/else
/endif
命令根据布尔表达式的结果在两个模版中选择一个:
"Hello, %{ if var.name != "" }${var.name}%{ else }unnamed%{ endif }!"
else
部分可以省略,这样如果布尔表达结果为false那么就会插入空字符串。
for \<NAME\> in \<COLLECTION\>
/endfor
命令迭代一个结构化对象或者集合,用每一个元素渲染模版,然后把它们拼接起来:
<<EOT
%{ for ip in aws_instance.example.*.private_ip }
server ${ip}
%{ endfor }
EOT
for
关键字后紧跟的名字被用作代表迭代器元素的临时变量,可以用来在内嵌模版中使用。
为了在不添加额外空格和换行的前提下提升可读性,所有的模版序列都可以在首尾添加 ~
符号。如果有 ~
符号,那么模版序列会去除字符串左右的空白(空格以及换行)。如果 ~
出现在头部,那么会去除字符串左侧的空白;如果出现在尾部,那么会去除字符串右边的空白:
<<EOT
%{ for ip in aws_instance.example.*.private_ip ~}
server ${ip}
%{ endfor ~}
EOT
上面的例子里,命令符后面的换行符被忽略了,但是 server ${ip}
后面的换行符被保留了,这确保了每一个元素生成一行输出:
server 10.1.16.154
server 10.1.16.1
server 10.1.16.34
当使用模版命令时,我们推荐使用 heredoc 风格字符串,用多行模版提升可读性。双引号字符串内最好只使用插值。
1.4.8.1.15. Terraform 插值
Terraform 曾经只支持在表达式中使用插值,例如
resource "aws_instance" "example" {
ami = var.image_id
# ...
}
这种语法是在 Terraform 0.12 后才被支持的。在 Terraform 0.11 及更早的版本中,这段代码只能被写成这样:
resource "aws_instance" "example" {
ami = "${var.image_id}"
# ...
}
Terraform 0.12 保持了向前兼容,所以现在这样的代码也仍然是合法的。读者们也许会在一些 Terraform 代码和文档中继续看到这样的写法,但请尽量避免继续这样书写纯插值字符串,而是直接使用表达式。