1.4.8.1. 表达式

表达式用来在配置文件中进行一些计算。最简单的表达式就是字面量,比如"hello",或者5。Terraform也支持一些更加复杂的表达式,比如引用其他resource的输出值、数学计算、布尔条件计算,以及一些内建的函数。

Terraform配置中很多地方都可以使用表达式,但某些特定的场景下限制了可以使用的表达式的类型,例如只准使用特定数据类型的字面量,或是禁止使用resource的输出值。

我们在类型章节中已经基本介绍了类型以及类型相关的字面量,下面我们来介绍一些其他的表达式。

1.4.8.1.1. 下标和属性

list和tuple可以通过下标访问成员,例如local.list[3]var.tuple[2]。map和object可以通过属性访问成员,例如local.object.attrnamelocal.map.keyname。由于map的key是用户定义的,可能无法成为合法的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_devicedevice_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语言支持一组算数和逻辑操作符,它们的功能类似于JavaScript或Ruby里的操作符功能。

当一个表达式中含有多个操作符时,它们的优先级顺序时:

  1. !- (负号)
  2. */%
  3. +- (减号)
  4. >>=<<=
  5. ==!=
  6. &&
  7. ||

可以使用小括号覆盖默认优先级。如果没有小括号,高优先级操作符会被先计算,例如1+2*3会被解释成1+(2*3)而不是(1+2)*3。

不同的操作符可以按它们之间相似的行为被归纳为几组,每一组操作符都期待被给予特定类型的值。Terraform会在类型不符时尝试进行隐式类型转换,如果失败则会抛错。

1.4.8.1.7.1. 算数操作符

  • a + b:返回ab的和
  • a - b:返回ab的差
  • a * b:返回ab的积
  • a / b:返回ab的商
  • a % b:返回ab的模。该操作符一般仅在ab是整数时有效
  • -a:返回a-1的商

1.4.8.1.7.2. 相等性操作符

  • a == b:如果ab类型与值都相等返回true,否则返回false
  • a != b:与==相反

1.4.8.1.7.3. 比较操作符

  • a < b:如果ab小则为true,否则为false
  • a > b:如果ab大则为true,否则为false
  • a <= b:如果ab小或者相等则为true,否则为false
  • a >= b:如果ab大或者相等则为true,否则为false

1.4.8.1.7.4. 逻辑操作符

  • a || bab中有至少一个为true则为true,否则为false
  • a && ba与比都为true则为true,否则为false
  • !a:如果atrue则为false,如果afalse则为true

1.4.8.1.8. 条件表达式

条件表达式是判断一个布尔表达式的结果以便于在后续两个值当中选择一个:

condition ? true_val : false_val

如果condition表达式为true,那么结果是true_value,反之则为false_value。

一个常见的条件表达式用法是使用默认值替代非法值:

var.a != "" ? 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可以在resourcedataproviderprovisioner块内使用。一个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块只能生成属于当前块定义过的内嵌块参数。无法生成诸如lifecycleprovisioner这样的元参数,因为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后才被支持的。在Terrafor 0.11及更早的版本中,这段代码只能被写成这样:

resource "aws_instance" "example" {
  ami           = "${var.image_id}"
  # ...
}

Terraform 0.12保持了向前兼容,所以现在这样的代码也仍然是合法的。读者们也许会在一些Terraform代码和文档中继续看到这样的写法,但请尽量避免继续这样书写纯插值字符串,而是直接使用表达式。

results matching ""

    No results matching ""