Terraform概述:一份配置怎样变成云上的资源?
假设现在有一份 Terraform 配置,描述了一台云服务器和一个安全组。第一次执行时,Terraform 会创建它们;改一下实例名称,下一次就可能只更新名称。
同样是 terraform apply,它怎么知道这次应该创建,下一次应该修改?又怎么知道服务器应该绑定刚刚创建的那个安全组?
这两个问题把 Terraform 的主要概念连了起来:配置表达期望,state 记录资源身份,Provider 查询和操作远端对象,Core 根据这些信息组织执行计划。
Core 与 Provider 怎样分工?
Terraform Core 负责读取配置、处理表达式、组织资源依赖,以及协调计划和执行。具体怎样调用腾讯云的接口,交给腾讯云 Provider。
可以按下面这条路径理解一次操作:
1 | .tf 配置与输入变量 |
Provider 定义了资源有哪些字段、怎样读取资源,以及哪些属性变化需要原地更新或替换。Core 和 Provider 会共同参与计划计算,不能仅凭 HCL 语法判断一次修改的后果。
这也解释了为什么换一个云平台时,CLI 命令可以保持一致,资源类型和参数却要重新学习。
先分清插件声明和插件配置
课件里的腾讯云示例使用这样的依赖声明:
1 | terraform { |
required_providers 指定使用哪个插件及允许的版本范围。provider 块则配置插件怎样访问平台,这里选择了广州地域。访问凭证可以由环境变量提供。
版本号沿用课程示例,用来解释约束语法,不代表当前推荐版本。~> 1.81.111 允许从 1.81.111 开始的 1.81.x 版本,不允许升级到 1.82.0。如果写成 ~> 1.81,允许的范围就会扩展到小于 2.0.0 的版本。
首次初始化后,Terraform 会把选中的 Provider 版本与校验信息写入 .terraform.lock.hcl。后续正常执行 init 时,只要锁定版本仍满足约束,就优先复用它。需要主动升级时再执行 terraform init -upgrade,并检查锁文件及计划的变化。
因此需要同时保留版本约束和锁文件。前者表达允许范围,后者记录这份配置实际选择的版本。这个锁文件主要锁 Provider,不会替你锁住所有远程模块版本。
读懂 HCL 中的几个名字
HCL 是 HashiCorp Configuration Language。Terraform 使用它编写配置,常见组成包括配置块、参数、表达式和引用。
1 | variable "environment" { |
variable 定义调用者可以传入的参数。locals 用来保存配置内部计算出来的值,例如多处会使用的名称前缀。resource 声明由 Terraform 管理的对象,output 则暴露执行结果或模块接口。
资源声明里的两个字符串尤其容易混淆:tencentcloud_security_group 是资源类型,web 是当前模块中的本地名称。云控制台里看到的名字由 name 参数决定。
tencentcloud_security_group.web.id 表示读取这个资源的 ID。创建之前,ID 可能还不知道,所以计划中会出现 (known after apply)。这种未知值通常是正常的,Terraform 会在执行获得结果后继续处理依赖它的资源。
字符串里混入其他内容时可以用 ${...},单纯引用一个值则直接写 var.environment 或资源属性即可。
查询已有信息,用 data
课件通过数据源查询公共镜像:
1 | data "tencentcloud_images" "ubuntu" { |
这里读取平台上的镜像信息,不会因为查询就把这些镜像纳入 Terraform 的创建和销毁管理。
resource 和 data 的区别,可以用后续责任来判断:前者通常负责管理对象生命周期,后者负责获取信息。若要接管一个已经存在的资源,还需要配置与导入流程,写一个数据源不能完成接管。
查询结果也可能变化。课件中直接使用结果列表的第一项,便于演示;要复现一个确定环境,就应检查过滤条件和查询结果,必要时固定镜像 ID 与实例规格。
资源创建顺序从哪里来?
假设实例配置中存在这一行:
1 | orderly_security_groups = [tencentcloud_security_group.web.id] |
实例引用了安全组的 ID,Terraform 就能识别依赖:先获得安全组,才能继续创建并绑定实例。.tf 文件叫 01-network.tf 还是 99-network.tf,不会决定这两个资源的执行顺序。
同一个目录下的 Terraform 配置文件会组成一个模块。工具依据引用关系建立依赖图;没有依赖的操作可以并行,存在依赖的操作则按关系安排。
有些依赖仅靠属性引用看不出来。课件中的实例除了绑定安全组,还要求安全组规则先创建完成,因为后续步骤马上需要通过 SSH 连接。实例没有直接引用规则资源的某个属性,这时可以补充:
1 | depends_on = [tencentcloud_security_group_lite_rule.web] |
这是一段资源块内部的配置片段。depends_on 补的是执行依赖,不能代替应用健康检查。机器已经创建,SSH 仍可能暂时不可用;Kubernetes API 能连接,某个业务 Pod 也未必已经准备好。
用一轮命令看清完整过程
在一个已经写好配置的目录中,可以执行:
1 | terraform init |
init 准备工作目录、初始化 backend,并获取需要的 Provider 和模块。fmt 检查排版,validate 检查配置语法和内部一致性。即使它们都通过,也不能证明云账号有足够配额、所选机型有库存或实例一定能够启动。
plan 读取相关状态并生成变更计划,通常会访问云 API,但不会执行计划中的资源创建、修改和删除。计划中的几个常见符号是:
| 标记 | 含义 | 需要关注的内容 |
|---|---|---|
+ |
新建 | 数量、规格、网络范围是否正确 |
~ |
原地更新 | 修改哪些属性,会不会影响访问 |
- |
删除 | 是否确实要移除这个对象 |
-/+ |
删除后重建 | 为什么需要替换,数据和访问入口怎样处理 |
+/- |
创建后删除 | 是否允许同时存在两个对象,配额是否足够 |
-out=tfplan 保存了这次计划。检查后执行 terraform apply tfplan,会直接应用已保存的计划,不再等待交互确认。若直接运行 terraform apply,Terraform 会重新生成计划并请求确认,两种用法要分清。
计划文件也可能包含敏感数据,不应提交到公开仓库。执行前若配置、state 或外部条件已经变化,应重新规划;旧计划并不保证永远可以成功应用。
state 不只是执行记录
state 把配置中的资源地址映射到真实对象,同时记录资源属性及管理所需的信息。它既可能保存在本地,也可以通过 backend 存储到远端。
如果丢失 state,仅有相同的配置,Terraform 也不一定能认出云上已经创建好的机器。再次执行可能尝试创建新资源,或者因为名称冲突而失败。这时需要恢复状态或导入资源,不能把“重新 apply 一次”当作通用修复。
几个常用命令的影响范围需要记清:
| 命令 | 作用 |
|---|---|
terraform state list |
列出 state 中记录的资源地址 |
terraform state show ADDRESS |
查看一个受管资源的已记录属性 |
terraform state pull |
将当前 backend 中的 state 输出到标准输出 |
terraform state rm ADDRESS |
移除管理映射,保留真实资源 |
terraform plan -refresh-only |
预览怎样根据远端现状更新 state 和输出 |
笔记中提到了 terraform refresh。现在更适合先使用 refresh-only 计划检查变化,再决定是否应用,让状态更新也有一次审查机会。
state rm 尤其不能当作“删除资源失败时的清理按钮”。它移除映射后,云服务器仍可能存在并计费;如果配置声明仍然保留,下次计划还可能尝试再建一个。
多人协作时,需要共享同一份环境状态,并根据 backend 的能力配置锁、访问权限和版本保护。支持状态锁的 backend 可以阻止冲突写入,但不同环境仍应使用不同的状态路径。远程存储解决不了环境混用和权限配置错误。
变量、文件和模块怎样组织?
单文件适合起步,配置增多后可以按职责拆开:
1 | terraform-demo/ |
versions.tf 写依赖约束,variables.tf 写输入声明,cvm.tf 写资源,outputs.tf 写输出。文件名是组织约定,Terraform 不会因为名字不同而分阶段执行它们,也不会自动把子目录里的资源一并加载。
变量值可以从默认值、环境变量和变量文件等位置提供。在常规本地 CLI 工作流中,覆盖优先级从低到高为:变量默认值、TF_VAR_ 环境变量、terraform.tfvars、terraform.tfvars.json、按文件名字典序加载的 *.auto.tfvars / *.auto.tfvars.json,最后是命令行中的 -var 与 -var-file。命令行赋值按出现顺序处理。
这也是“明明改了默认值,运行结果却没变”的一个排查方向:检查是否有优先级更高的赋值。
需要复用一组资源时,再提取 module。子模块声明输入,管理内部资源,通过 output 暴露结果;调用它的根模块决定使用哪组环境参数。下一篇的 CVM 与 K3s 就适合用这种接口连接。
count 与 for_each 解决什么问题?
count 按数量创建实例,资源地址使用数字索引,例如 web[0]。相同配置的多个副本,用它比较直接。
如果实例对应一个会增删成员的名称列表,就要考虑索引变化。删除列表中间的元素,后续元素会前移,可能触发意料之外的更新或替换。for_each 使用 map 的键或字符串 set 的元素作为身份,例如 web["api"],更适合需要稳定名称的对象。
这里可以区分三个容易混在一起的概念:for 表达式转换集合值;for_each 创建多个资源或模块实例;dynamic 块生成重复的嵌套配置块。它们解决的问题不同,不能互相替换。
生命周期规则能帮到哪一步?
lifecycle 可以调整资源变更行为,几个常见选项包括:
create_before_destroy:资源替换时先尝试创建新对象,再删除旧对象。它需要额外配额,也可能受到唯一名称限制。业务零停机还要配合流量切换和就绪检查。prevent_destroy:配置保留该规则时,阻止会销毁对应资源的计划。如果整个资源块被移除,规则也随之消失,不能把它当作平台级的永久删除保护。ignore_changes:在规划更新时忽略指定属性的变化,适合确实由其他系统维护的属性。范围过大,会把应该检查的漂移也隐藏掉。replace_triggered_by:根据指定受管资源或其属性的变更触发替换。
我会在能明确说出“默认行为哪里不合适”时再加这些规则。否则配置虽然更长,维护者却更难看清哪些差异会被忽略、哪些对象会被替换。
最后还有 provisioner,可以在本地或远程执行命令、传输文件。课件用它下载 K3s 的 kubeconfig,很适合理解系统之间如何衔接。不过脚本内部的副作用并不会自动变成 Terraform 可完整管理的资源。日常软件配置仍可以考虑镜像预置、cloud-init 或 Ansible。
下一篇《Terraform实践》会把这些概念落到一台腾讯云服务器上:先生成计划,创建并检查资源,再做一次小范围修改,最后验证清理结果。
参考资料
- Terraform 配置语言
- Terraform CLI
- Dependency lock file
- Terraform state
- 课程代码:
lesson/module_2/demo_1至demo_5。

