TT Lab
开始
学习 学习路径 课程

Terraform/OpenTofu 基础

手动拼接值,最后输给了引号

在 TT Lab 中继续学习

目标

先在 console 中逐行确认字符串·集合·条件·转换·模板·序列化函数,再把它们原样搬到配置中,创建真正的文件。最后创建一份不直接写任何值、只从变量中取值的配置,确认它在其他输入下是否也能经受住考验。

为什么重要

配置文件变长的原因,通常是把同样的值写在了多个地方。如果用函数写出名称的组合规则、各环境不同的值、合并映射时的优先级这类内容,修改一个值时,需要改动的地方就缩减为一处。而且函数在 console 中用一行就能确认,表达式有疑问时,反复应用所花的成本就消失了。处理失败的函数同样重要。从外部传入的数据随时可能损坏,如果每次都导致整个应用停下来,运维就被别人的失误绑住了手脚。了解把失败转换为值的函数,就能设计出退回默认值的方案。最后,序列化函数让人不必再为引号和转义负责——手工拼接出来的 JSON,迟早一定会损坏。

步骤

  1. 创建 /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。
  2. 在 /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。
  3. 在 /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。
  4. 在 /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。
  5. 在 /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;、}。
  6. 在 /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。两个文件的内容必须相同。
  7. 在 /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。
  8. 在 /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 中确认字符串函数

创建 /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 会并排遍历多个列表,并应用同样的格式。标题中的数量必须通过计数得出,这样即使输入变了也是对的。做到这一步,就不必为改一个值而修改文件的多个地方了。