造值那一侧的语法
一句话总结
内置函数让配置文件中的值可以通过计算得出,而其结果在应用之前,就能在 console 中用一行确认。
为什么需要函数
声明式配置给人的第一印象是“把值原样写出来”。但规模稍微变大,就会出现无法原样写出的东西。名称要由环境、服务和编号组合而成,标签要把公共部分和各自的部分合并,端口则因环境而异。如果把这些都写成固定的值,每增加一个环境,需要修改的地方就会分散在许多处。
更糟糕的是手工拼接的序列化。如果用字符串相加来构造 JSON 配置,引号和转义就要由人来负责,只要值里混进一个双引号,它就会损坏。所以语言中引入了函数。
工作原理
函数大致分为几类:处理字符串的、处理集合的、转换类型的、处理失败的,以及模板和序列化。
join("-", ["web", "prod", "01"]) -> "web-prod-01"
format("%s-%03d", "node", 7) -> "node-007"
merge({ a = 1 }, { a = 9, b = 2 }) -> { a = 9, b = 2 } 뒤가 이긴다
lookup({ web = 80 }, "api", 0) -> 0 없으면 기본값
distinct(["a", "b", "a"]) -> ["a", "b"]
formatlist("%s=%d", names, ports) -> 두 목록을 나란히 돌며 서식 적용
条件表达式的形式是 조건 ? 참일때 : 거짓일때(韩文,意为“条件 ? 为真时 : 为假时”)。两个分支的类型必须相同,不同的话,工具会试图把它们统一成一个,从而产生莫名其妙的转换。
处理失败的两个函数性质不同。try 在前一个参数求值失败时会给出下一个参数,can 则把是否成功转换成真假。有一个重要的限制——两者都只能捕捉运行中产生的错误。引用根本没有声明的变量或 locals,是静态错误,即使用 try 包起来也没用。
locals {
cfg = try(jsondecode(file("${path.module}/in.json")), {}) # 깨진 JSON 이면 빈 맵
name = try(local.cfg.name, "unknown") # 키가 없으면 기본값
}
当有多个值时,使用模板。在模板文件中可以使用循环和条件指令,并通过添加去除空白的标记,使指令所在的行不会在结果中留下空行。
%{ for p in ports ~}
listen ${p};
%{ endfor ~}
序列化函数把值转换成 JSON 或 YAML 的表示。不必手工加引号,而且最重要的是,重新读回来,得到的是同样的数据。把同一个映射用两个函数分别导出,再各自读回来,就能确认两者完全相同。
类型转换函数的好处是没有悄无声息的失败。遇到无法转换的值,当场就会报错。
tonumber("007") -> 7
parseint("ff", 16) -> 255
tostring(true) -> "true"
tonumber("abc") -> 오류
try(tonumber("abc"), -1) -> -1
而不经过应用就能确认这一切的地方,就是 console。console 会读取当前目录的变量和 locals,所以即使不修改配置,也可以用其他变量值去评估其他的分支。
在现场相遇的样子
最常见的,是漏掉了 lookup 默认值的代码。如果相信映射中键总是存在而这样写,在添加新环境的那天,就只有那个环境的应用会停下来。只要养成给出默认值的习惯,这一类事故就会整个消失。
第二种是把 try 误当成盾牌。值不对时悄悄退回到默认值,并不总是对的。如果配置本来是错的,却用默认值让应用成功了,问题就会在几周之后的生产环境中暴露。它适合用于从外部传入的数据,而不适合用于我们自己仓库里的值。
第三种是不使用模板而用字符串拼接出来的配置。起初因为短,看上去没有问题,但只要加入两个条件,就变得无法阅读。如果把多行的产出物拆到模板文件中,只看那个文件就能推测出结果。
下一项实验要做什么
走完八个步骤。在 console 中确认六个字符串函数,把合并和提取映射的函数的结果实际作为输出导出,并用条件表达式选出各环境的值。接着创建能吸收损坏 JSON 的配置,用模板生成六行的配置文件,把同样的数据以两种格式导出,读回来确认是否相同。最后两步,确认转换函数及其失败,并创建一份不直接写任何值的配置,让评分器用不同的输入去应用,确认结果是否会随之变化。