变量优先级、输出,以及对敏感值的错觉
一句话总结
要让同一份代码应对 dev、stage 和 prod,值就必须移到代码之外。问题在于可输入值的位置有很多,而且它们之间存在明确的优先级。
为什么需要了解这一点
假设有三个环境,区别仅在于实例大小和副本数。最简单的办法是复制出三套目录。半年后,这三个目录已经变成了不同的生物:只有 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 日志积存在公司所有人都能查看的位置,它就会成为泄漏路径。这正是除了敏感标记外,还必须同时检查日志保留策略的原因。
确认值究竟来自哪里
为一个变量提供值的通道有六种。不理解优先级,就会遇到“明明改了却 没有变化”。越往下优先级越高。
- 默认值(
default) - 环境变量
TF_VAR_이름 terraform.tfvars*.auto.tfvars(按文件名顺序)- 通过
-var-file=指定的文件 - 通过
-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_id,app_server_id
更持久;相比 ip,private_endpoint 也更持久。
terraform output -json | jq -r '.app_endpoint.value'
下个练习将做什么
在 /root/tf/vars 中,分别通过默认值、tfvars、命令行和环境变量四种方式为同一变量赋值,并把哪一个胜出记录到文件中。使用 validation 拒绝错误值并保存其消息。用 locals 组合名称前缀,并创建至少四项带说明的输出。最后声明敏感变量,亲眼确认它在供人阅读的输出中会被遮住,但值仍会保留在 JSON 中。