☰
Terraform 管理腾讯云 CVM:从 provider 到 state 的实战指南
2026/9/26 1:24:44 网站建设 项目流程

1. 为什么我最终把云主机管理交给了 Terraform

第一次接触 Terraform 是在一个需要同时管理十几台云主机的项目里。当时我的做法很原始:登录控制台,点"新建实例",选镜像、选规格、配安全组、设密钥,一套流程走下来五分钟,十台就是五十分钟,而且每台的配置还得靠记忆保证一致。更麻烦的是,某台机器被误删之后,我根本想不起来它当初的规格和网络配置,只能凭印象重建,结果和原来的环境对不上,排查了半天才发现是安全组规则少了一条。

这种"手工点控制台"的方式在机器数量少的时候还能忍,一旦上了规模,或者需要频繁重建环境,就会变成灾难。Terraform 解决的正是这个问题:它把云上的资源用代码描述出来,你写一份配置文件,执行一条命令,它就能帮你把整套基础设施创建出来;改配置再执行,它会算出差异并只改动需要改的部分。这就是所谓的基础设施即代码(Infrastructure as Code,IaC)。

这篇内容我打算从零讲清楚两件事:Terraform 的核心工作模型到底是什么,以及怎么用它创建一台腾讯云 CVM(Cloud Virtual Machine,云服务器)。关键词里提到的provider、terraform init这些概念,我会在对应的环节里逐个拆开讲。不管你是刚听说 Terraform 的新手,还是已经用过但一直没搞明白 state 文件到底在干嘛的老手,应该都能从里面找到有用的东西。我踩过的坑、绕过的弯路,也会一并写出来,省得你再走一遍。

2. Terraform 的工作模型:provider、state 与执行计划

2.1 provider 是 Terraform 和云平台之间的翻译官

Terraform 本身其实不认识"腾讯云""CVM"这些东西。它只是一个通用的编排引擎,真正知道怎么调用腾讯云 API 去创建一台服务器的,是provider。你可以把 provider 理解成一个插件或者驱动:Terraform 把"我要一台 2 核 4G 的机器"这种声明翻译成 provider 能懂的调用,provider 再去调腾讯云的接口把机器开出来。

腾讯云的 provider 叫tencentcloud,由腾讯云官方维护。它内部封装了几乎所有腾讯云产品的 API:CVM、VPC、CBS、CLB、COS 等等。你在配置文件里写resource "tencentcloud_instance",Terraform 就知道要去调 provider 里对应的创建逻辑。

这里有个新手最容易困惑的点:provider 是需要单独下载的,它不在 Terraform 主程序里。你写完配置第一次执行terraform init,Terraform 会去 registry 上找tencentcloud这个 provider,下载对应版本的二进制文件放到当前目录的.terraform文件夹里。所以terraform init这一步不是可有可无的仪式,它是真正在拉取依赖。网络不通或者版本写错,这一步就会失败,后面全都跑不起来。

2.2 state 文件:Terraform 的"账本"

如果说 provider 是翻译官,那state 文件(默认叫terraform.tfstate)就是 Terraform 的账本。它记录了一件极其重要的事:你配置文件里声明的资源,和云上真实存在的资源,是怎么一一对应的。

举个例子。你配置里写了要创建一台 CVM,执行terraform apply之后,云上多了一台机器,同时 state 文件里会记下这台机器的 ID、IP、规格等一堆属性。下次你改了配置再执行,Terraform 会拿新配置和 state 里的记录做对比,算出"要新增什么、要改什么、要删什么",然后生成一个**执行计划(plan)**给你确认。

为什么 state 这么关键?因为云平台的 API 大多不提供"按名字查资源"这种方便的语义。Terraform 要管理一台机器,必须知道它的唯一 ID,而 state 就是存这个 ID 的地方。如果你把 state 文件删了,Terraform 就"失忆"了,它会认为云上那台机器不存在,下次 apply 会再创建一台新的,于是你就有了两台——这就是典型的"state 丢失导致资源重复创建"事故。

注意:state 文件里可能包含敏感信息(比如数据库密码、密钥),所以它绝对不能提交到公开的代码仓库。团队协作时要用远程后端(比如对象存储)来存 state,并开启加密和锁机制。

2.3 声明式与执行计划:先看后做

Terraform 是声明式的。这句话的意思是,你只需要描述"我最终想要什么状态",而不需要写"第一步做什么、第二步做什么"。至于怎么从当前状态走到目标状态,是 Terraform 自己算的。

这个"算"的过程就产出执行计划。执行计划里会用符号标出每个动作:

符号含义说明
+创建云上还没有这个资源,需要新建
-销毁配置里删掉了,云上要删掉
~更新原地修改,比如改标签
-/+重建无法原地改,只能先删后建

我强烈建议每次 apply 之前都先跑一次terraform plan,把计划从头到尾看一遍。尤其是看到-/+重建的时候要格外小心——有些资源重建意味着数据丢失,比如一块云硬盘被重建,上面的数据就没了。养成"先 plan 后 apply"的习惯,能帮你挡掉绝大多数误操作。

3. 动手前的准备:账号、密钥与工具链

3.1 拿到腾讯云的 API 密钥

Terraform 要替你操作腾讯云,就必须有凭证。腾讯云用的是SecretId和SecretKey这一对密钥,在控制台的"访问管理"里可以创建。创建的时候有几点经验:

  • 不要用主账号的密钥。主账号密钥权限太大,一旦泄露后果严重。正确做法是创建一个子用户(CAM 用户),只授予它需要的那部分权限,比如 CVM 的创建和查询权限。
  • 密钥只在创建时显示一次 SecretKey,关掉页面就再也看不到了,务必当场保存好。
  • 给密钥起个能看懂的名字,比如terraform-cvm-dev,方便以后轮换和排查。

拿到密钥之后,不要直接写死在配置文件里。Terraform 支持从环境变量读取,这是最省事也最安全的做法:

export TENCENTCLOUD_SECRET_ID="你的SecretId" export TENCENTCLOUD_SECRET_KEY="你的SecretKey"

provider 会自动读取这两个环境变量,配置文件里就不用出现明文密钥了。如果你在 CI/CD 环境里跑,也是同样的思路,把密钥配成流水线的环境变量即可。

3.2 安装 Terraform 与版本选择

Terraform 是一个单二进制文件,官网下载解压后放到 PATH 里就能用。装完执行terraform version确认一下。

版本选择上有个坑要提醒:Terraform 从 1.6 版本开始换了开源协议,后续版本和早期版本在生态上有些差异。对于腾讯云 provider 来说,目前主流版本都能正常工作,但我建议用 1.5 以上的稳定版,避免太老的版本遇到 provider 语法不兼容的问题。同时,provider 本身也有版本,写配置时最好把 provider 版本约束住,比如version = "~> 1.81",这样团队里每个人拉到的 provider 版本一致,不会出现"我这儿能跑你那儿报错"的情况。

3.3 目录结构:别把所有东西塞一个文件

新手常见的做法是把所有配置写进一个main.tf。小项目无所谓,但只要资源一多,这个文件就会变成几百行的怪物,改起来痛苦。我习惯的目录结构是这样的:

terraform-cvm/ ├── main.tf # 主资源定义 ├── provider.tf # provider 和版本约束 ├── variables.tf # 变量声明 ├── outputs.tf # 输出定义 └── terraform.tfvars # 变量赋值(不提交敏感值)

这样拆开之后,找东西、改配置都清爽很多。variables.tf里声明变量,terraform.tfvars里给具体值,敏感的值通过环境变量或密钥管理工具注入,职责分明。

4. 写一份能跑通的 CVM 配置

4.1 provider 块:告诉 Terraform 用哪朵云

先写provider.tf:

terraform { required_version = ">= 1.5.0" required_providers { tencentcloud = { source = "tencentcloudstack/tencentcloud" version = "~> 1.81" } } } provider "tencentcloud" { region = var.region }

这里source指定了 provider 的来源地址,version约束了版本范围。region用变量传入,方便不同环境切换。注意 provider 块里没有写密钥,因为它会从前面设置的环境变量里读。

4.2 变量声明:把可变的部分抽出来

variables.tf:

variable "region" { description = "腾讯云地域,例如 ap-guangzhou" type = string default = "ap-guangzhou" } variable "instance_type" { description = "CVM 规格" type = string default = "S5.MEDIUM4" } variable "image_id" { description = "镜像 ID" type = string } variable "password" { description = "实例登录密码" type = string sensitive = true }

把地域、规格、镜像、密码抽成变量,好处是同一份配置能复用到不同环境。sensitive = true会让 Terraform 在输出时把值打码,避免密码出现在日志里。

4.3 核心资源:创建一台 CVM

main.tf里最关键的部分:

resource "tencentcloud_instance" "web" { instance_name = "terraform-demo-web" availability_zone = "${var.region}-1" image_id = var.image_id instance_type = var.instance_type system_disk_type = "CLOUD_PREMIUM" system_disk_size = 50 internet_max_bandwidth_out = 5 allocate_public_ip = true password = var.password security_groups = [tencentcloud_security_group.web.id] tags = { Environment = "dev" ManagedBy = "terraform" } }

逐行拆一下几个关键参数:

  • availability_zone:可用区。地域是ap-guangzhou,可用区就是ap-guangzhou-1这种。写错可用区会直接报错。
  • image_id:镜像决定了操作系统。这个值必须从控制台或 API 查,不能瞎猜。后面我会讲怎么查。
  • instance_type:规格,比如S5.MEDIUM4表示 2 核 4G。规格和可用区是绑定的,某些规格只在特定可用区有货。
  • internet_max_bandwidth_out:公网出带宽,单位 Mbps。设成大于 0 才会分配公网 IP。
  • allocate_public_ip:是否分配公网 IP。内网机器设 false 就行。
  • security_groups:安全组 ID 列表。这里引用了另一个资源,Terraform 会自动处理依赖顺序——先建安全组,再建机器。

4.4 安全组:别让机器裸奔

机器建出来如果安全组没配好,要么完全连不上,要么端口全开。我习惯显式定义一个安全组:

resource "tencentcloud_security_group" "web" { name = "terraform-demo-sg" description = "安全组由 Terraform 管理" } resource "tencentcloud_security_group_rule" "ssh" { security_group_id = tencentcloud_security_group.web.id type = "ingress" protocol = "tcp" port_range = "22" cidr_ip = "你的办公网出口IP/32" description = "仅允许办公网 SSH" }

这里cidr_ip我特意写成你自己的出口 IP 加/32,意思是只放行这一个地址。很多教程图省事写0.0.0.0/0,等于把 SSH 端口对全世界开放,非常危险。生产环境一定要收紧来源。

4.5 输出:把有用的信息打出来

outputs.tf:

output "instance_id" { value = tencentcloud_instance.web.id } output "public_ip" { value = tencentcloud_instance.web.public_ip }

apply 完成后,这两个值会打印在终端上,方便你直接拿去连机器。输出也可以被其他 Terraform 配置引用,做模块化的时候很有用。

5. 从 init 到 apply:完整执行链路与踩坑记录

5.1 terraform init 到底做了什么

配置写好后,第一步永远是:

terraform init

这一步做了三件事:初始化后端(决定 state 存哪儿)、下载 provider 插件、初始化模块。第一次跑会看到它在下载tencentcloudprovider,速度取决于网络。如果卡住或者报错,八成是网络问题,可以配置镜像源加速。

我遇到过的 init 报错里,最常见的是版本约束写错,比如version = "~> 1.81"写成了不存在的版本号,Terraform 会提示找不到匹配的 provider 版本。这时候把约束放宽或者改成实际存在的版本就行。

5.2 plan 阶段:把差异看清楚

terraform plan

plan 会输出一份详细的执行计划。第一次跑,所有资源前面都是+,表示全新创建。输出里会列出每个资源的属性,包括那些你没写、由 provider 补默认值的字段。我建议第一次 plan 的时候认真读一遍,看看有没有意外的默认值,比如系统盘类型、带宽计费方式这些。

有个细节值得说:plan 里显示的某些属性是(known after apply),意思是这个值要等资源真正创建出来才知道,比如实例 ID、公网 IP。这是正常的,不用管。

5.3 apply 阶段:确认与执行

terraform apply

它会再算一次计划,然后停下来问你Do you want to perform these actions?,输入yes才会真正执行。这个交互设计就是为了防止手滑。执行过程中会实时打印每个资源的创建进度,全部完成后输出结果。

如果中途失败,比如某个规格在目标可用区售罄,Terraform 会报错并停止。这时候 state 里可能已经记录了部分创建成功的资源。你可以修正配置后重新 apply,Terraform 会接着把没建完的补上,已经建好的不会重复创建——这正是 state 的价值。

5.4 我踩过的几个坑

坑一:镜像 ID 猜错。我一开始以为镜像 ID 是通用的,随手填了个网上的示例,结果报"镜像不存在"。镜像 ID 是地域相关的,广州的镜像 ID 在北京不一定有。正确做法是用数据源动态查询:

data "tencentcloud_images" "ubuntu" { image_type = ["PUBLIC_IMAGE"] os_name = "ubuntu" } resource "tencentcloud_instance" "web" { image_id = data.tencentcloud_images.ubuntu.images[0].image_id # ... 其他配置 }

这样每次 apply 都会自动选到最新的 Ubuntu 公共镜像,不用手动维护 ID。

坑二:密码不符合复杂度要求。腾讯云对实例密码有复杂度要求,必须包含大小写字母、数字、特殊符号中的至少三类,长度 8 到 30 位。我第一次设了个纯数字密码,apply 直接失败。后来改成Tf@2024Demo!这种才通过。

坑三:state 文件被误删。有一次我清理目录,顺手把terraform.tfstate删了,结果下次 apply 又创建了一台新机器,账单上多了一台。从那以后我养成了两个习惯:一是把 state 放到远程后端,二是本地操作前先terraform state list看一眼当前管理了哪些资源。

坑四:安全组规则方向搞反。type字段有ingress(入站)和egress(出站)两个值。我一开始把 SSH 规则写成了 egress,结果怎么都连不上,排查了半天才发现方向错了。入站规则管的是"谁能进来",出站管的是"能出去访问谁",别搞混。

6. 资源变更、销毁与日常维护

6.1 改配置之后会发生什么

假设你想把机器规格从 2 核 4G 升到 4 核 8G,只需要改instance_type变量,然后 plan。这时候你会看到-/+符号,表示这台机器需要重建。为什么不能原地改?因为云平台的规格变更往往涉及底层宿主机的迁移,API 层面就是"删了重建"。

重建意味着系统盘数据会丢。如果机器上有重要数据,一定要先备份,或者把数据盘独立出来(数据盘可以设置不随实例销毁)。这是 Terraform 使用中最需要警惕的一类操作。

6.2 只销毁部分资源

有时候你只想删掉某台机器,保留其他资源。可以用:

terraform destroy -target=tencentcloud_instance.web

-target参数能精确指定要操作的目标。不过这个参数要慎用,它会让 state 和配置产生偏差,用完之后最好再跑一次完整的 plan 确认状态一致。

6.3 完整的销毁

terraform destroy

它会列出所有将被删除的资源,确认后全部清理。这一步同样要先看清楚计划,别把不该删的删了。生产环境我一般会禁用 destroy,或者要求二次确认。

6.4 日常维护的几个习惯

  • 定期 plan:即使不改配置,也定期跑一次 plan,看看有没有"漂移"——也就是有人绕过 Terraform 在控制台上手动改了东西。plan 显示有差异,就说明实际状态和配置对不上了。
  • state 加锁:团队协作时,远程后端要开启锁,防止两个人同时 apply 导致 state 冲突。
  • 版本固定:provider 和 Terraform 版本都固定住,避免自动升级带来的意外。
  • 标签规范:给所有资源打上统一的标签,比如ManagedBy = "terraform",方便识别哪些资源是 Terraform 管的,哪些是手工建的。

7. 把单机配置升级成可复用模块

7.1 为什么要模块化

当你需要创建十台配置类似的机器时,复制十份 resource 块是最笨的做法。Terraform 的**模块(module)**机制允许你把一组资源打包成一个可复用的单元,通过传参来定制。

7.2 一个简单的 CVM 模块

把前面创建 CVM 的逻辑抽到一个modules/cvm目录里,输入变量是规格、镜像、数量等,输出是实例 ID 列表。调用的时候:

module "web_cluster" { source = "./modules/cvm" instance_count = 3 instance_type = "S5.MEDIUM4" image_id = data.tencentcloud_images.ubuntu.images[0].image_id password = var.password }

模块内部用count或for_each批量创建:

resource "tencentcloud_instance" "this" { count = var.instance_count instance_name = "web-${count.index + 1}" # ... 其他配置 }

这样一份配置就能拉起三台机器,改数量只改一个数字。模块化的价值在规模上来之后会非常明显,维护成本直线下降。

7.3 模块使用的注意事项

模块的source可以是本地路径,也可以是远程仓库或 registry。团队内部共享模块时,建议用版本化的远程地址,比如 Git 仓库的某个 tag,这样模块升级可控。另外,模块的输入输出要设计得清晰,别把一堆内部实现细节暴露出去,否则调用方会很难用。

8. 一些实战中的经验与提醒

关于密钥管理,我再强调一次:环境变量只是入门做法,生产环境应该用专门的密钥管理服务,或者至少把密钥放在 CI/CD 的加密变量里。密钥要定期轮换,离职人员涉及的密钥要立即作废。

关于成本,Terraform 本身不产生费用,但它创建的资源会产生费用。建议在测试环境用按量计费的实例,用完及时 destroy。我见过有人忘了销毁测试机器,一个月下来账单多出好几百。可以在配置里给测试资源打上AutoDestroy = "true"之类的标签,配合定时任务做清理。

关于 provider 版本升级,不要盲目追新。升级前先在测试环境跑一遍 plan,确认没有破坏性变更再上生产。腾讯云 provider 的更新日志里会标注 breaking changes,升级前务必读一遍。

关于 state 的备份,远程后端一般自带版本控制,但本地开发时也建议定期备份terraform.tfstate。这个文件一旦损坏,恢复起来非常麻烦。

最后说一个心态上的体会:Terraform 的学习曲线在前两天比较陡,provider、state、plan 这几个概念绕在一起容易懵。但只要亲手跑通一次"创建一台 CVM"的完整流程,把 init、plan、apply、destroy 四个命令走一遍,再回头理解这些概念就会豁然开朗。我当初就是卡在"为什么要 init"这个问题上,后来想明白它是在下载 provider,整个模型就通了。建议你也别光看,找个测试账号,照着上面的配置实际跑一遍,踩几个坑,比看十篇文章都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询