手动拼接值,最后输给了引号
目标
先在 console 中逐行确认字符串·集合·条件·转换·模板·序列化函数,再把它们原样搬到配置中,创建真正的文件。最后创建一份不直接写任何值、只从变量中取值的配置,确认它在其他输入下是否也能经受住考验。
为什么重要
配置文件变长的原因,通常是把同样的值写在了多个地方。如果用函数写出名称的组合规则、各环境不同的值、合并映射时的优先级这类内容,修改一个值时,需要改动的地方就缩减为一处。而且函数在 console 中用一行就能确认,表达式有疑问时,反复应用所花的成本就消失了。处理失败的函数同样重要。从外部传入的数据随时可能损坏,如果每次都导致整个应用停下来,运维就被别人的失误绑住了手脚。了解把失败转换为值的函数,就能设计出退回默认值的方案。最后,序列化函数让人不必再为引号和转义负责——手工拼接出来的 JSON,迟早一定会损坏。
步骤
- 创建
/root/tfb-func/play,用只声明了 local provider 的main.tf执行 init。在/root/tfb-func/strings-in.txt中写入六行——join("-", ["web", "prod", "01"])、format("%s-%03d", "node", 7)、replace("db-prod-01", "prod", "stage")、trimspace(" padded ")、substr("abcdefgh", 2, 3)、upper(join("_", sort(["b", "a"])))。把该文件通过管道送入tofu console,把结果保存到/root/tfb-func/strings-out.txt。 - 在
/root/tfb-func/coll/main.tf中放置map(number)变量ports(web 80,api 8080),用merge合并两个标签映射的local.tags,用sort(keys(...))创建的local.names,用lookup读取的local.api(存在的键)和local.db(不存在的键,默认值 5432),以及用distinct去除重复的local.unique,并以五个输出导出。执行 init·apply 之后,把tofu output -json保存到/root/tfb-func/collections.json。 - 在
/root/tfb-func/cond/main.tf中放置默认值为prod的变量env、装有var.env == "prod"的local.is_prod、用它来选择的local.replicas(3 或 1)、local.log_level(warn 或 debug)、local.backup(daily 或 none)、用format创建的local.endpoint(端口为 443 或 8080),并创建把这四个值按키=값(韩文,意为“键=值”)格式写成四行的app.conf。执行 init·apply 之后,用tofu console -var env=dev对同样的四个值求值,保存到/root/tfb-func/cond-dev.txt。 - 在
/root/tfb-func/guard/中创建正确的 JSONgood.json(name为api,port为 8080)和损坏的 JSONbad.json。在main.tf中用try(jsondecode(...), {})读取这两个文件,分别用try(..., "unknown")取出存在的键和不存在的键,并用can(jsondecode(...))记录两个文件是否成功,以五个输出(name·miss·bad_keys·ok_good·ok_bad)导出。apply 之后,把tofu output -json保存到/root/tfb-func/guard.json。 - 在
/root/tfb-func/tpl/site.tftpl中编写一个模板,包含名称一行、遍历端口列表的循环,以及取决于是否启用 TLS 的条件;在main.tf中用templatefile传入name = "api"、ports = [80, 443]、tls = true,创建site.conf。结果必须以server {开头,共六行:name = api、listen 80;、listen 443;、tls on;、}。 - 在
/root/tfb-func/enc/main.tf中放置local.data(name= api,ports= [80, 443],tls= true),并用jsonencode创建data.json,用yamlencode创建data.yaml。apply 之后,把用jq -c .读取的结果保存到/root/tfb-func/enc-json.txt,把用yq -o=json -I=0 .读取的结果保存到/root/tfb-func/enc-yaml.txt。两个文件的内容必须相同。 - 在
/root/tfb-func/convert-in.txt中写入六行——parseint("ff", 16)、tonumber("007")、tostring(true)、try(tonumber("abc"), -1)、can(tonumber("abc"))、format("%s has %d ports", "api", length([80, 443]))。在/root/tfb-func/play中把它通过管道送入 console,把结果保存到/root/tfb-func/convert-out.txt。 - 在
/root/tfb-func/compose/main.tf中放置list(string)变量names(默认 api、web、db)和list(number)变量ports(默认 8080、80、5432),用formatlist创建이름=포트(韩文,意为“名称=端口”)行列表,用join连接,用format和length创建# <개수> services(占位符为数量)标题,并创建services.conf。第一行是标题,其后是服务行。不要直接写值,只能从变量中取值——评分器会用不同的输入来应用同一份配置。
参考
- console 会读取当前目录的变量和 locals,所以必须在已经完成 init 的目录中运行。
- console 遇到第一个错误就会停止,并以非 0 的码结束。通过管道送入多行时,前面的行成功了,后面的行才会被求值。
- 常见错误:把第 4 步中损坏的 JSON 修好。那个文件必须保持损坏状态,才能确认吸收是否生效。
- 常见错误:在第 8 步把服务数量写死为 3。输入一变,就只有标题是错的。
- 函数列表 · templatefile · try · can · jsonencode · yamlencode · 条件表达式
在 console 中确认字符串函数
创建 /root/tfb-func/play,用只声明了 local provider 的 main.tf 执行 init。在 /root/tfb-func/strings-in.txt 中写入六行——join("-", ["web", "prod", "01"])、format("%s-%03d", "node", 7)、replace("db-prod-01", "prod", "stage")、trimspace(" padded ")、substr("abcdefgh", 2, 3)、upper(join("_", sort(["b", "a"])))。把该文件通过管道送入 tofu console,把结果保存到 /root/tfb-func/strings-out.txt。
把文件通过管道送入 console,它会逐行求值,只打印结果。format 的占位符完全遵循格式字符串的规则,而 substr 的第二个参数不是长度,而是起始位置。
合并并提取映射和列表
在 /root/tfb-func/coll/main.tf 中放置 map(number) 变量 ports(web 80,api 8080),用 merge 合并两个标签映射的 local.tags,用 sort(keys(...)) 创建的 local.names,用 lookup 读取的 local.api(存在的键)和 local.db(不存在的键,默认值 5432),以及用 distinct 去除重复的 local.unique,并以五个输出导出。执行 init·apply 之后,把 tofu output -json 保存到 /root/tfb-func/collections.json。
merge 中靠后的映射胜出。lookup 的第三个参数是键不存在时的默认值,漏掉它的话,遇到不存在的键就会报错。keys 不保证排序,所以用 sort 包起来。
用条件表达式选出各环境的值
在 /root/tfb-func/cond/main.tf 中放置默认值为 prod 的变量 env、装有 var.env == "prod" 的 local.is_prod、用它来选择的 local.replicas(3 或 1)、local.log_level(warn 或 debug)、local.backup(daily 或 none)、用 format 创建的 local.endpoint(端口为 443 或 8080),并创建把这四个值按 키=값(韩文,意为“键=值”)格式写成四行的 app.conf。执行 init·apply 之后,用 tofu console -var env=dev 对同样的四个值求值,保存到 /root/tfb-func/cond-dev.txt。
条件表达式中,为真时和为假时的类型必须相同。给 console 传入变量值,就可以在不修改配置的情况下看到其他分支的结果,无需反复应用就能完成确认。
把损坏的输入吸收为值
在 /root/tfb-func/guard/ 中创建正确的 JSON good.json(name 为 api,port 为 8080)和损坏的 JSON bad.json。在 main.tf 中用 try(jsondecode(...), {}) 读取这两个文件,分别用 try(..., "unknown") 取出存在的键和不存在的键,并用 can(jsondecode(...)) 记录两个文件是否成功,以五个输出(name·miss·bad_keys·ok_good·ok_bad)导出。apply 之后,把 tofu output -json 保存到 /root/tfb-func/guard.json。
try 在前一个参数求值失败时给出下一个参数,can 则把是否成功转换成真假。两者都只捕捉运行中产生的错误——引用根本没有声明这类静态错误,它们是捕捉不到的。
用模板生成配置文件
在 /root/tfb-func/tpl/site.tftpl 中编写一个模板,包含名称一行、遍历端口列表的循环,以及取决于是否启用 TLS 的条件;在 main.tf 中用 templatefile 传入 name = "api"、ports = [80, 443]、tls = true,创建 site.conf。结果必须以 server { 开头,共六行:name = api、listen 80;、listen 443;、tls on;、}。
模板中的循环和条件是以百分号开头的指令。要使指令所在的行不在结果中留下空行,就在右花括号前加上去除空白的标记。模板文件的扩展名可以随意,但有惯例。
把同样的数据以两种格式导出
在 /root/tfb-func/enc/main.tf 中放置 local.data(name = api,ports = [80, 443],tls = true),并用 jsonencode 创建 data.json,用 yamlencode 创建 data.yaml。apply 之后,把用 jq -c . 读取的结果保存到 /root/tfb-func/enc-json.txt,把用 yq -o=json -I=0 . 读取的结果保存到 /root/tfb-func/enc-yaml.txt。两个文件的内容必须相同。
这两个函数只是把同一个值转换为不同的表示,所以读回来得到的是同样的数据。如果用这些函数,而不是手工拼接配置文件,引号和转义就由工具来负责。
确认转换函数及其失败
在 /root/tfb-func/convert-in.txt 中写入六行——parseint("ff", 16)、tonumber("007")、tostring(true)、try(tonumber("abc"), -1)、can(tonumber("abc"))、format("%s has %d ports", "api", length([80, 443]))。在 /root/tfb-func/play 中把它通过管道送入 console,把结果保存到 /root/tfb-func/convert-out.txt。
转换函数遇到无法转换的值会报错。把这个错误转换为值的是 try,转换为真假的是 can。对于进制不是 10 的字符串,另有专用的函数。
把函数串联起来,汇总成一个配置文件
在 /root/tfb-func/compose/main.tf 中放置 list(string) 变量 names(默认 api、web、db)和 list(number) 变量 ports(默认 8080、80、5432),用 formatlist 创建 이름=포트(韩文,意为“名称=端口”)行列表,用 join 连接,用 format 和 length 创建 # <개수> services(占位符为数量)标题,并创建 services.conf。第一行是标题,其后是服务行。不要直接写值,只能从变量中取值——评分器会用不同的输入来应用同一份配置。
formatlist 会并排遍历多个列表,并应用同样的格式。标题中的数量必须通过计数得出,这样即使输入变了也是对的。做到这一步,就不必为改一个值而修改文件的多个地方了。