表达式用来在配置文件中进行一些计算,最简单的表达式就是字面量,比如”hello”,或者5。Terraform也支持一些更加复杂的表达式,比如引用其他resource
的输出值、数学计算、布尔条件计算,以及一些内建的函数。
在Terraform配置中很多地方都可以使用表达式,但某些特定的场景下限制了可以使用的表达式的类型,例如只准使用特定数据类型的字面量,或是禁止使用resource
的输出值等。
下标和属性
list
和tuple
可以通过下标访问成员,例如local.list[3]、var.tuple[2]。map
和object
可以通过属性访问成员,例如local.object.attrname、local.map.keyname。由于map的key是用户定义的,可能无法成为合法的Terraform标识符,所以访问map成员时推荐使用方括号:local.map[“keyname”]。
引用命名值
Terraform中定义了多种命名值,表达式中的每一个命名值都关联到一个具体的值,可以用单一命名值作为一个表达式,或是组合多个命名值来计算出一个新值。
命名值有如下种类:
<RESOURCE TYPE>.<NAME>
:表示一个资源对象,如果资源声明了count
元参数,那么该表达式表示的是一个对象实例的list;如果资源声明了for_each
元参数,那么该表达式表示的是一个对象实例的mapvar.<NAME>
:表示一个输入变量local.<NAME>
:表示一个局部值module.<MODULE_NAME>.<OUTPUT_NAME>
:表示一个模块的一个输出值data.<DATA_TYPE>.<NAME>
:表示一个数据源实例,如果数据源声明了count
元参数,那么该表达式表示的是一个数据源实例list;如果数据源声明了for_each
元参数,那么该表达式表示的是一个数据源实例mappath.module
:表示当前模块在文件系统中的路径path.root
:表示根模块(调用Terraform命令行执行的代码文件所在的模块)在文件系统中的路径path.cwd
:表示当前工作目录的路径,一般来说该路径等同于path.root,但在调用Terraform命令行时如果指定了代码路径,那么二者将会不同terraform.workspace
:当前使用的Workspace
虽然这些命名表达式可以使用.<NAME>
方式来访问对象的各种属性,但实际上他们实际类型并不是数据类型object。两者的区别在于,object同时支持使用.<NAME>
或者["<NAME>"]
两种方式访问对象成员属性,而上述命名表达式仅支持.<NAME>
。
局部命名值
在某些特定表达式或上下文当中,有一些特殊的命名值可以被使用,他们是局部命名值。
几种比较常见的局部命名值有:
count.index
:表达当前count下标序号each.key
:表达当前for_each迭代器实例self
:在预置器中指代声明预置器的资源
命名值的依赖关系
构建资源或是模块时经常会使用含有命名值的表达式赋值,Terraform会分析这些表达式并自动计算出对象之间的依赖关系。
引用资源输出属性
最常见的引用类型就是引用一个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
参数可以通过展开表达式来访问,比如获取所有的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
注意:
(1)使用for_each
的资源集合不能直接使用展开表达式,展开表达式只能适用于列表
(2)可以把字典转换成列表后再使用展开表达式:values(aws_instance.example)[*].id
尚不知晓的值
当Terraform在计算变更计划时,有些资源输出属性无法立即求值,因为它们的值取决于远程API的返回值。例如,有一个远程对象可以在创建时返回一个生成的唯一id,Terraform无法在创建它之前就预知这个值。
为了允许在计算变更阶段就能计算含有这种值的表达式,Terraform使用了一个特殊的”尚不知晓(unknown value)”占位符
来代替这些结果。大部分时候不需要特意理会它们,因为Terraform语言会自动处理这些尚不知晓的值,比如说使两个尚不知晓的值相加得到的会是一个尚不知晓的值。
然而,有些情况下表达式中含有尚不知晓的值会有明显的影响:
count
元参数不可以为尚不知晓,因为变更计划必须明确地知晓到底要维护多少个目标实例- 如果尚不知晓的值被用于数据源,那么数据源在计算变更计划阶段就无法读取,它会被推迟到执行阶段读取,这种情况下在计划阶段该数据源的一切输出均为尚不知晓
- 如果声明
module
块时传递给模块输入变量的表达式使用了尚不知晓值,那么在模块代码中任何使用了该输入变量值的表达式的值都将是尚不知晓 - 如果模块输出值表达式中含有尚不知晓值,任何使用该模块输出值的表达式都将是尚不知晓
- Terraform会尝试验证尚不知晓值的数据类型是否合法,但仍然有可能无法正确检查数据类型,导致执行阶段发生错误
尚不知晓值在执行terraform plan
时会被输出为”(not yet known)”。
算数和逻辑操作符
Terraform语言支持一组算数和逻辑操作符,它们的功能类似于JavaScript或Ruby里的操作符功能。
当一个表达式中含有多个操作符时,它们的优先级顺序时:
!
,-
(负号)*
,/
,%
+
,-
(减号)>
,>=
,<
,<=
==
,!=
&&
||
可以使用小括号覆盖默认优先级。
不同的操作符可以按它们之间相似的行为被归纳为几组,每一组操作符都期待被给予特定类型的值。
Terraform会在类型不符时尝试进行隐式类型转换,如果失败则会抛错。
算数操作符
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的商
相等性操作符
a == b
:如果a与b类型与值都相等返回true,否则返回falsea != b
:与==相反
比较操作符
a < b
:如果a比b小则为true,否则为falsea > b
:如果a比b大则为true,否则为falsea <= b
:如果a比b小或者相等则为true,否则为falsea >= b
:如果a比b大或者相等则为true,否则为false
逻辑操作符
a || b
:a或b中有至少一个为true则为true,否则为falsea && b
:a与比都为true则为true,否则为false!a
:如果a为true则为false,如果a为false则为true
条件表达式
条件表达式是判断一个布尔表达式的结果以便于在后续两个值当中选择一个:
condition ? true_val : false_val
如果condition表达式为true,那么结果是true_value,反之则为false_value。
一个常见的条件表达式用法是使用默认值替代非法值:
var.a != "" ? var.a : "default-a"
如果输入变量a的值是空字符串,那么结果会是default-a,否则返回输入变量a的值。
条件表达式的判断条件可以使用上述的任意操作符,供选择的两个值也可以是任意类型,但它们的类型必须相同,这样Terraform才能判断条件表达式的输出类型。
函数调用
Terraform支持在计算表达式时使用一些内建函数,函数调用表达式类似操作符,通用语法是:
<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
函数名标明了要调用的函数,每一个函数都定义了数量不等、类型不一的入参以及不同类型的返回值。
有些函数定义了不定长的入参表,例如,min函数可以接收任意多个数值类型入参,返回其中最小的数值:
min(55, 3453, 2)
展开函数入参
如果想要把列表或元组的元素作为参数传递给函数,可以使用展开符:
min([55, 2453, 2]...)
展开符使用的是三个独立的.
号组成的...
,展开符是一种只能用在函数调用场景下的特殊语法。
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 != ""}
展开表达式
展开表达式提供了一种类似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参数我们也不需要修改这个表达式。
dynamic块
在顶级块(例如resource
)当中,一般只能以类似name = expression
的形式进行一对一的赋值。大部分情况下这已经够用了,但某些资源类型包含了可重复的内嵌块,无法使用表达式循环赋值:
resource "aws_elastic_beanstalk_environment" "tfenvtest" {
name = "tf-test-name"
# 在aws_elastic_beanstalk_environment中通常会包含多个setting块
setting {
namespace = "aws:ec2:vpc"
name = "VPCId"
value = "vpc-xxxxxxxx"
}
setting {
namespace = "aws:ec2:vpc"
name = "Subnets"
value = "subnet-xxxxxxxx"
}
}
可以用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是一样的,这时不应该使用keyvalue
:当前元素的值
一个dynamic
块只能生成属于当前块定义过的内嵌块参数,无法生成诸如lifecycle
、provisioner
这样的元参数,因为Terraform必须在确保对这些元参数求值的计算是成功的。
for_each
的值必须是不为空的map或者set,如果你需要根据内嵌数据结构或者多个数据结构的元素组合来声明资源实例集合,可以使用Terraform表达式和函数来生成合适的值。
注意: 过度使用dynamic
块会导致代码难以阅读以及维护,建议只在需要构造可重用的模块代码时使用dynamic块,尽可能手写内嵌块。
字符串字面量
Terraform有两种不同的字符串字面量,最通用的就是用一对双引号包裹的字符,比如”hello”。在双引号之间,反斜杠\被用来进行转义。
Terraform支持的转义符有:
符号 | 说明 |
---|---|
\n |
换行 |
\r |
回车 |
\t |
制表符 |
\" |
双引号 (不会截断字符串) |
\\ |
反斜杠 |
\uNNNN |
普通字符映射平面的Unicode字符(NNNN代表四位16进制数) |
\UNNNNNNNN |
补充字符映射平面的Unicode字符(NNNNNNNN代表八位16进制数) |
另一种字符串表达式被称为”heredoc”风格,是受Unix Shell语言启发。它可以使用自定义的分隔符更加清晰地表达多行字符串:
<<EOT
hello
world
EOT
<<
标记后面直到行尾组成的标识符(上述例子为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风格两种字符串都支持字符串模版,模版的形式是${...}
以及%{...}
。如果想要表达${
或者%{
的字面量,那么可以重复第一个字符:$${
和%%{
。
字符串模版
字符串模版允许在字符串中嵌入表达式,或是通过其他值动态构造字符串。
插值
一个${...}
序列被称为插值,插值计算花括号之间的表达式的值,有必要的话将之转换为字符串,然后插入字符串模版,形成最终的字符串:
"Hello, ${var.name}!"
输入变量var.name
的值被访问后插入了字符串模版,产生了最终的结果,比如:”Hello, Juan!” 。
命令
一个%{...}
序列被称为命令,命令可以是一个布尔表达式或者是对集合的迭代,类似条件表达式以及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风格字符串,用多行模版提升可读性;双引号字符串内最好只使用插值。
【参考】
Expressions
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达,在下面评论区告诉我^_^^_^