LabHub
学习 学习路径 课程

Terraform/OpenTofu 基础

变量优先级、输出,以及对敏感值的错觉

在 LabHub 中继续学习

一句话总结

要让同一份代码应对 dev、stage 和 prod,值就必须移到代码之外。问题在于可输入值的位置有很多,而且它们之间存在明确的优先级

概念图: 明确的优先级 · validation 的真正价值是让失败更早发生 · 具名表达式 · 只会在屏幕上隐藏输出

为什么需要了解这一点

假设有三个环境,区别仅在于实例大小和副本数。最简单的办法是复制出三套目录。半年后,这三个目录已经变成了不同的生物:只有 prod 包含某项安全设置,stage 中没有;dev 修复的缺陷也没有反映到 prod。事故总是在这种缝隙里发生。

变量就是防止这种复制的机制。只保留一份代码,仅替换值,环境间的差异便会从“三套文件的 diff”缩减为“几个值”,人们也就能够直接目视确认差异。

工作原理

输入值的位置有多个,后来的值胜出。

优先级 位置 特性
最低 变量声明中的 default “无人提供时使用这个值”
较低 TF_VAR_이름 环境变量 从 CI runner 或 shell 注入
中等 terraform.tfvars 当前目录的标准值
较高 *.auto.tfvars(按文件名字典序) 自动一并读取的值
-var-file=파일 为本次执行选择的一组值
最高 -var 이름=값 仅本次执行使用的例外

变量声明除了值之外,还可以附加三样内容。type 能尽早阻止错误的形式,description 说明该值的含义,validation 则用代码固定允许范围。validation 的真正价值是让失败更早发生。没有验证时,错误的值会在应用进行到一半时以 provider 错误爆发,而此时已有一半资源创建完成。

locals 看似变量,性质却不同。它是无法从外部注入的具名表达式。比如由多个变量组合成的名称前缀,适合用它定义一次后在多处使用。

output 是这项配置的公开接口。作为模块使用时,它是其他代码引用的唯一窗口,因此最好为每项输出都添加 description。再加上 -json 选项,输出便会成为机器可读格式,很适合用于流水线。

最后是 sensitive = true。它只会在屏幕上隐藏输出。状态文件中仍以明文保留该值,output -json 中也会原样包含该值。如果不知道这一点,就可能误以为“标记为敏感就安全了”,从而把状态文件随意放置。

实际工作中的表现

第一,用 -var 扑灭后便忘记的火。 如果在事故处置期间通过 -var replicas=10 恢复了服务,该值之后必须提交到存储库。否则,下次正常部署会悄悄恢复为旧值。命令行值具有最高优先级,这一点在紧急情况下很有用,却也要付出不留记录的代价。

第二,默认值的陷阱。 如果为了“先运行起来”而给所有变量都设置默认值,那么没有提供值时,可能会悄悄按开发环境配置创建生产环境。对每个环境都必须不同的值,不设默认值反而更安全。缺少值时工具会询问或失败,而这种失败的成本远低于事故。

第三,留在计划日志中的秘密。 plan 输出会原样打印资源属性。如果把 CI 日志积存在公司所有人都能查看的位置,它就会成为泄漏路径。这正是除了敏感标记外,还必须同时检查日志保留策略的原因。

确认值究竟来自哪里

为一个变量提供值的通道有六种。不理解优先级,就会遇到“明明改了却 没有变化”。越往下优先级越高。

  1. 默认值(default
  2. 环境变量 TF_VAR_이름
  3. terraform.tfvars
  4. *.auto.tfvars(按文件名顺序)
  5. 通过 -var-file= 指定的文件
  6. 通过 -var= 直接指定的值

在同一个文件里写两次会报错,但通过不同通道提供时会悄悄覆盖。 如果 CI 预先注入了 TF_VAR_,而你在本地修改 terraform.tfvars,第三项会覆盖第二项, 因此可能在本地有效、在 CI 中却无效。若要确认值来自哪里,应把计划保存为文件后查看。

terraform plan -out=tfplan
terraform show -json tfplan | jq '.variables'

验证应附加在变量声明上。 如果在 apply 进行到一半时才遇到错误值, 一半资源已经创建完毕。

variable "env" {
  type = string
  validation {
    condition     = contains(["dev", "stg", "prod"], var.env)
    error_message = "env 는 dev, stg, prod 중 하나여야 합니다."
  }
}

sensitive = true 只会遮住屏幕显示。 状态文件和计划文件仍会原样包含该值。 秘密最好不要通过变量传递,而应从秘密存储中作为数据源读取;即便如此,也要以它仍会 留在状态中为前提来管理存储库。

输出是留给下一个人的接口。 模块的使用方会依赖模块输出,因此一旦确定就很难修改。 与其原样导出内部使用的资源名称,不如按含义命名:相比 instance_idapp_server_id 更持久;相比 ipprivate_endpoint 也更持久。

terraform output -json | jq -r '.app_endpoint.value'

下个练习将做什么

/root/tf/vars 中,分别通过默认值、tfvars、命令行和环境变量四种方式为同一变量赋值,并把哪一个胜出记录到文件中。使用 validation 拒绝错误值并保存其消息。用 locals 组合名称前缀,并创建至少四项带说明的输出。最后声明敏感变量,亲眼确认它在供人阅读的输出中会被遮住,但值仍会保留在 JSON 中。