表达式
基础类型
基础类型分三类:string、number、bool。
string代表一组 Unicode 字符串,例如:"hello"。number代表数字,可以为整数,也可以为小数。bool代表布尔值,要么为true,要么为false。bool值可以被用做逻辑判断。
number 和 bool 都可以和 string 进行隐式转换,当我们把 number 或 bool 类型的值赋给 string 类型的值,或是反过来时,Packer 会自动替我们转换类型,其中:
true值会被转换为"true",反之亦然false值会被转换为"false",反之亦然15会被转换为"15",3.1415会被转换为"3.1415",反之亦然
复杂类型
复杂类型是一组值所组成的符合类型,有两类复杂类型。
一种是集合类型。一个集合包含了一组同一类型的值。集合内元素的类型成为元素类型。一个集合变量在构造时必须确定集合类型。集合内所有元素的类型必须相同。
Packer 支持三种集合:
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"}。键可以不用双引号,但如果键是以数字开头则例外。多对键值对之间要用逗号分隔,也可以用换行符分隔。推荐使用=号(HCL 代码规范中规定按等号对齐,使用等号会使得代码在格式化后更加美观)set(...):集合类型,代表一组不重复的值。
以上集合类型都支持通配类型缩写,例如 list 等价于 list(any),map 等价于 map(any),set 等价于 set(any)。any 代表支持任意的元素类型,前提是所有元素都是一个类型。例如,将 list(number) 赋给 list(any) 是合法的,list(string) 赋给 list(any) 也是合法的,但是 list 内部所有的元素必须是同一种类型的。
第二种复杂类型是结构化类型。一个结构化类型允许多个不同类型的值组成一个类型。结构化类型需要提供一个 schema 结构信息作为参数来指明元素的结构。
Packer 支持两种结构化类型:
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]
复杂类型也支持隐式类型转换。
Packer 会尝试转换相似的类型,转换规则有:
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 转换举的例子。
如果类型不匹配,Packer 会报错,例如我们试图把 object({name = ["Kristy", "Claudia", "Mary Anne", "Stacey"], age = 12}) 转换到 map(string) 类型,这是不合法的,因为 name 的值为 list,无法转换为 string。
any
any 是 Packer 中非常特殊的一种类型约束,它本身并非一个类型,而只是一个占位符。每当一个值被赋予一个由 any 约束的复杂类型时,Packer 会尝试计算出一个最精确的类型来取代 any。
例如我们把 ["a", "b", "c"] 赋给 list(any),它在 Packer 中实际的物理类型首先被编译成 tuple([string, string, string]),然后 Packer 认为 tuple 和 list 相似,所以会尝试将它转换为 list(string)。然后 Packer 发现 list(string) 符合 list(any) 的约束,所以会用 string 取代 any,于是赋值后最终的类型是 list(string)。
由于即使是 list(any),所有元素的类型也必须是一样的,所以某些类型转换到 list(any) 时会对元素进行隐式类型转换。例如将 ["a", 1, "b"] 赋给 list(any),Packer 发现 1 可以转换到 "1",所以最终的值是 ["a", "1", "b"],最终的类型会是 list(string)。再比如我们想把 ["a", \[\], "b"] 转换成 list(any),由于 Packer 无法找到一个一个合适的目标类型使得所有元素都能成功隐式转换过去,所以 Packer 会报错,要求所有元素都必须是同一个类型的。
声明类型时如果不想有任何的约束,那么可以用 any:
variable "no_type_constraint" {
type = any
}
这样的话,Packer 可以将任何类型的数据赋予它。
null
存在一种特殊值是无类型的,那就是 null。null 代表数据缺失。如果我们把一个参数设置为 null,Packer 会认为你忘记为它赋值。如果该参数有默认值,那么 Packer 会使用默认值;如果没有又恰巧该参数是必填字短,Packer 会报错。null 在条件表达式中非常有用,你可以在某项条件不满足时跳过对某参数的赋值。
字面表达式(Literal Expressions)
字面表达式是直接表示特定常量值的表达式。 Packer 对上述每种值类型都有一个文字表达式语法:
- 字符串通常由 Unicode 字符的双引号序列表示,
"like this"。对于更复杂的字符串,还有一种“heredoc”语法。字符串文字是 Packer 中最复杂的一种文字表达式,本页另有详细说明: - 数字由带或不带小数点的不带引号的数字序列表示,例如
15或6.283185。 - 布尔值由未加引号的符号
true和false表示。 null值由未加引号的符号null表示。list/tuple由一对包含以逗号分隔的值序列的方括号表示,如["a", 15, true]。 列表文字可以分成多行以提高可读性,但始终需要在值之间使用逗号。允许在最终值后使用逗号,但不是必需的。列表中的值可以是任意表达式。map/object由一对包含一系列<KEY> = <VALUE>对的花括号表示:
{
name = "John"
age = 52
}
键/值对可以用逗号或换行符分隔。值可以是任意表达式。键必须是字符串;如果它们是有效标识符,则可以不加引号,否则必须加引号。您可以通过将非字面表达式括在括号中来将其用作键,例如 (var.business_unit_tag_name) = "SRE"。
下标和属性
list 和 tuple 可以通过下标访问成员,例如local.list[3]、var.tuple[2]。map和object可以通过属性访问成员,例如local.object.attrname、local.map.keyname。由于 map 的键是用户定义的,可能无法成为合法的 Packer 标识符,所以访问 map 成员时我们推荐使用方括号:local.map["keyname"]。
引用命名值
Packer 目前只有一种命名值可用:
source.<SOURCE TYPE>.<NAME>是表示给定类型和名称的source对象。
算数和逻辑操作符
一个操作符是一种用以转换或合并一个或多个表达式的表达式。操作符要么是把两个值计算为第三个值,也就是二元操作符;要么是把一个值转换成另一个值,也就是一元操作符。
二元操作符位于两个表达式的中间,类似 1+2。一元操作符位于一个表达式的前面,类似!true。
Packer 语言支持一组算数和逻辑操作符,它们的功能类似于 JavaScript 或 Ruby 里的操作符功能。
当一个表达式中含有多个操作符时,它们的优先级顺序时:
!,-(负号)*,/,%+,-(减号)>,>=,<,<===,!=&&||
可以使用小括号覆盖默认优先级。如果没有小括号,高优先级操作符会被先计算,例如 1+2\*3 会被解释成 1+(2\*3) 而不是 (1+2)\*3。
不同的操作符可以按它们之间相似的行为被归纳为几组,每一组操作符都期待被给予特定类型的值。Packer 会在类型不符时尝试进行隐式类型转换,如果失败则会抛错。
算数操作符
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 的值。
条件表达式的判断条件可以使用上述的任意操作符。供选择的两个值也可以是任意类型,但它们的类型必须相同,这样 Packer 才能判断条件表达式的输出类型。
函数调用
Packer 支持在计算表达式时使用一些内建函数,函数调用表达式类似操作符,通用语法是:
<FUNCTION NAME>(<ARGUMENT 1>, <ARGUMENT 2>)
函数名标明了要调用的函数。每一个函数都定义了数量不等、类型不一的入参以及不同类型的返回值。
有些函数定义了不定长的入参表,例如,min 函数可以接收任意多个数值类型入参,返回其中最小的数值:
min(55, 3453, 2)
完整的函数列表请参阅文档。
展开函数入参
如果想要把列表或元组的元素作为参数传递给函数,那么我们可以使用展开符:
min([55, 2453, 2]...)
展开符使用的是三个独立的.号组成的...,不是Unicode中的省略号…。展开符是一种只能用在函数调用场景下的特殊语法。
有关完整的内建函数我们可能会在今后撰写相应的章节介绍。
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 != ""}
展开表达式(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参数我们也不需要修改这个表达式。
dynamic块
在顶级块,例如 source 块当中,一般只能以类似 name = expression 或是 key = expression 的形式进行一对一的赋值。大部分情况下这已经够用了,但某些类型的 source 包含了可重复的内嵌块,无法使用表达式循环赋值:
source "amazon-ebs" "example" {
name = "pkr-test-name" # can use expressions here
tag {
# but the "tag" block is always a literal block
}
}
你可以用dynamic 块来动态构建重复的 tag 这样的内嵌块:
locals {
standard_tags = {
Component = "user-service"
Environment = "production"
}
}
source "amazon-ebs" "example" {
# ...
tag {
key = "Name"
value = "example-asg-name"
}
dynamic "tag" {
for_each = local.standard_tags
content {
key = tag.key
value = tag.value
}
}
}
一个 dynamic 块类似于 for 表达式,只不过它产生的是内嵌块。它可以迭代一个复杂类型数据然后为每一个元素生成相应的内嵌块。在上面的例子里:
dynamic的标签(也就是tag)确定了我们要生成的内嵌块种类for_each参数提供了需要迭代的复杂类型值iterator参数(可选)设置了用以表示当前迭代元素的临时变量名。如果没有设置iterator,那么临时变量名默认就是dynamic块的标签(也就是tag)labels参数(可选)是一个表示块标签的有序列表,用以按次序生成一组内嵌块。有labels参数的表达式里可以使用临时的iterator变量- 内嵌的
content块定义了要生成的内嵌块的块体。你可以在content块内部使用临时的iterator变量
由于 for_each 参数可以是集合或者结构化类型,所以你可以使用 for 表达式或是展开表达式来转换一个现有集合的类型。
iterator 变量(上面的例子里就是tag)有两个属性:
key:迭代容器如果是map,那么就是当前元素的键;迭代容器如果是list,那么就是当前元素在list中的下标序号;如果是由for_each表达式产出的set,那么key和value是一样的,这时我们不应该使用key。value:当前元素的值
一个 dynamic 块只能生成属于当前块定义过的内嵌块参数。
for_each 的值必须是不为 null 的 map 或者 set。如果你需要根据内嵌数据结构或者多个数据结构的元素组合来声明资源实例集合,你可以使用 Packer 表达式和函数来生成合适的值。
dynamic块的最佳实践
过度使用 dynamic 块会导致代码难以阅读以及维护,所以我们建议只在需要构造可重用的模块代码时使用 dynamic 块。尽可能手写内嵌块。
字符串字面量
Packer 有两种不同的字符串字面量。最通用的就是用一对双引号包裹的字符,比如 "hello"。在双引号之间,反斜杠 \ 被用来进行转义。Packer 支持的转义符有:
| Sequence | Replacement |
|---|---|
| \n | 换行 |
| \r | 回车 |
| \t | 制表符 |
| \" | 双引号 (不会截断字符串) |
| \\ | 反斜杠 |
| \uNNNN | 普通字符映射平面的Unicode字符(NNNN代表四位16进制数) |
| \UNNNNNNNN | 补充字符映射平面的Unicode字符(NNNNNNNN代表八位16进制数) |
另一种字符串表达式被称为 "heredoc" 风格,是受 Unix Shell 语言启发。它可以使用自定义的分隔符更加清晰地表达多行字符串:
<<EOT
hello
world
EOT
<< 标记后面直到行尾组成的标识符开启了字符串,然后 Packer 会把剩下的行都添加进字符串,直到遇到与标识符完全相等的字符串为止。在上面的例子里,EOT 就是标识符。任何字符都可以用作标识符,但传统上标识符一般以 EO 起头。上面例子里的 EOT 代表"文本的结束(end of text)"。
上面例子里的 heredoc 风格字符串要求内容必须对齐行头,这在块内声明时看起来会比较奇怪:
block {
value = <<EOT
hello
world
EOT
}
为了改进可读性,Packer 也支持缩进的 heredoc,只要把 << 改成 <<-:
block {
value = <<-EOT
hello
world
EOT
}
上面的例子里,Packer 会以最靠近行头的行作为基准来调整行头缩进,得到的字符串是这样的:
hello
world
heredoc 中的反斜杠不会被解释成转义,而只会是简单的反斜杠。
双引号和 heredoc 两种字符串都支持字符串模版,模版的形式是 ${...} 以及 %{...}。如果想要表达 ${ 或者 %{ 的字面量,那么可以重复第一个字符:$${和%%{ 。
字符串模版
字符串模版允许我们在字符串中嵌入表达式,或是通过其他值动态构造字符串。
插值(Interpolation)
一个${...}序列被称为插值,插值计算花括号之间的表达式的值,有必要的话将之转换为字符串,然后插入字符串模版,形成最终的字符串:
"Hello, ${var.name}!"
上面的例子里,输入变量 var.name 的值被访问后插入了字符串模版,产生了最终的结果,比如:"Hello, Juan!"
命令(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 var.servers[*].ip }
server ${ip}
%{ endfor }
EOT
for 关键字后紧跟的名字(ip)被用作代表迭代器元素的临时变量,可以用来在内嵌模版中使用。
为了在不添加额外空格和换行的前提下提升可读性,所有的模版序列都可以在首尾添加 ~ 符号。如果有 ~ 符号,那么模版序列会去除字符串左右的空白(空格以及换行)。如果 ~ 出现在头部,那么会去除字符串左侧的空白;如果出现在尾部,那么会去除字符串右边的空白:
<<EOT
%{ for ip in var.servers[*].ip ~}
server ${ip}
%{ endfor ~}
EOT
上面的例子里,命令符后面的换行符被忽略了,但是server ${ip}后面的换行符被保留了,这确保了每一个元素生成一行输出:
server 10.1.16.154
server 10.1.16.1
server 10.1.16.34
当使用模版命令时,我们推荐使用 heredoc 风格字符串,用多行模版提升可读性。双引号字符串内最好只使用插值。