Terraform实践:从一台腾讯云服务器到可复用的环境
前两篇《IaC和Terraform概述》和《Terraform概述》把配置、Provider 和 state 串了起来。开始操作之前,还需要弄清凭证放在哪里、创建失败后怎样继续,以及实验结束时怎么确认资源已经清理干净。
课程的 module_2 恰好提供了一条逐渐展开的路线:先创建腾讯云 CVM 和安全组,再引入远程状态,接着安装 K3s、部署 Argo CD,最后提取模块管理不同环境。
下面先整理一个可以独立操作的 CVM 实验,再分析后面的组合方式。示例基于课程代码调整,需要填入自己的云账号资源参数;这里没有提供一次真实云部署的成功记录。
先确定这次实验管理哪些资源
这次创建一台 CVM、一个安全组和对应规则,复用已有的 VPC、子网。这样第一次实验就可以集中观察资源创建、依赖和 state,清理时也不会涉及已有网络的生命周期。
准备时需要确认:
- 已安装 Terraform,
terraform version能正常输出版本。 - 云账号具备相关资源的查询和管理权限,并有可用额度。
- 已有 VPC 和子网,子网所属地域、可用区与实例配置一致。
- 已确认可用的 Ubuntu 镜像 ID、实例规格和系统盘类型。
- 知道当前管理端的公网出口 IP,用于限制 SSH 的来源。
课件直接查询可用区、镜像和机型,并取结果列表的第一项。这种写法容易看懂,但几个独立列表的第一项未必能组成一套可用配置。这里把结果改为显式输入,先在同一可用区确认一组可购买的参数,再交给 Terraform。
实验资源会产生费用,包括实例和可能产生的网络费用。创建前检查计费方式,用完后检查销毁结果。
配置文件怎么写?
新建一个空目录,放入下面四个文件。它们共同组成一个根模块。
1 | terraform-cvm-demo/ |
versions.tf:依赖与访问地域
Provider 版本范围沿用课程 demo_2,用于这份示例。升级版本时要重新查看计划,不能把这里的旧版本范围当作长期维护建议。
1 | terraform { |
凭证通过 Provider 支持的环境变量读取,不写进配置。下面的 shell 命令会静默读取输入,避免把真实密钥直接写进命令文本:
1 | # 在 Bash 中执行 |
variables.tf:把环境选择留在输入侧
地域、可用区、镜像和实例规格没有通用的正确组合,所以这里不替它们提供默认值。管理端 CIDR 也需要自己填写,例如单个出口地址应使用 /32。
1 | variable "region" { |
创建自己的 terraform.tfvars,填写上述非敏感参数。可以使用下面这份模板,但所有占位值都要替换,地域和可用区也要按自己的环境确认:
1 | region = "ap-guangzhou" |
实例密码单独输入:
1 | # 在 Bash 中执行 |
sensitive = true 会隐藏常规输出中的明文显示,但相关值仍可能进入 state 和保存的计划文件。它解决显示问题,存储保护还需要依靠文件权限和 backend 配置。
main.tf:资源与依赖
1 | resource "tencentcloud_security_group" "web" { |
实例对安全组 ID 的引用会形成隐式依赖。这里另外等待规则资源完成,是为了让创建结束后的 SSH 检查有明确前提。
课件部分示例采用竞价实例 SPOTPAID,并开放 SSH 和 K3s API。这个起步实验改为按量计费,只开放来自管理端的 SSH,先减少竞价回收和集群安装带来的变量。按量实例同样需要检查账户余额、库存与资源配额。
outputs.tf:输出后续需要的信息
1 | output "instance_id" { |
后续检查资源需要 ID,连接主机需要 IP。密码已经由输入侧提供,不必再输出一次。
第一次执行,怎么判断每一步是否完成?
先在实验目录运行:
1 | terraform init |
在一个没有既有 state 的新目录里,按这份配置生成的计划应包含安全组、规则和实例三个受管资源的创建。数据和环境条件不同,输出细节可能不同,但不应该出现对已有 VPC、子网的删除。
检查实例规格、磁盘、计费方式、网络和入站来源后,再执行:
1 | terraform apply tfplan |
应用保存的计划不会再次询问 yes,前一步的审查就是确认动作。执行成功后,state list 应包含这三个地址:
1 | tencentcloud_instance.web |
这只能证明 Terraform 已经记录了资源。还要到云控制台核对实例状态、网络和安全组,再尝试 SSH:
1 | ssh ubuntu@<terraform-output-public-ip> |
这里的 ubuntu 需要与实际镜像的登录用户一致。如果连不上,先检查管理端出口 IP 是否变化、22 端口规则是否正确、实例是否完成启动,以及镜像是否允许所配置的认证方式。
不要因为 SSH 不通就立刻重新创建机器。若问题是来源地址不匹配,重建不会解决它。
再改一次,观察 Terraform 怎样处理变化
在 terraform.tfvars 中增加或修改:
1 | instance_name = "tf-learning-web-v2" |
然后重新规划:
1 | terraform plan -out=rename.tfplan |
由于安全组名称也引用了 instance_name,计划可能同时包含实例和安全组的名称更新。以实际计划为准;如果出现替换,先检查原因,再决定是否执行。
最后一个命令的退出码为 0 时表示没有差异,2 表示计划中有变化,1 表示错误。在输入和远端资源都没有继续变化的前提下,期望看到无变更。
第一次 apply 若中途失败,成功创建的资源通常已经进入 state。先定位失败原因,保留状态,再生成新计划,查看剩余工作。清空 state 会让问题变成“云上有资源,Terraform 却不再认识它”。
从本地 state 迁移到 COS
个人练习可以先用本地状态。换电脑或者交给另一个人执行时,就需要避免各自拿着不同的状态记录工作。
课程 demo_3 展示了 COS backend。目标 Bucket 必须预先存在,并且执行者具有访问权限。学习阶段可以通过独立配置或控制台准备 Bucket,避免让同一套短期实验同时负责保存和销毁自己的状态存储。
在实验根模块新增 backend.tf:
1 | terraform { |
Bucket 名称要填写完整名称,backend 的地域对应 Bucket 所在地域。这里使用示例前缀,其他环境应使用独立路径。backend 配置不能直接使用普通的 var.* 引用。
迁移前先备份当前状态,再初始化迁移:
1 | umask 077 |
核对迁移提示中的来源和目标。完成后,应能在 COS 中看到状态对象,资源地址保持一致,计划也不应因为迁移而突然要求重建环境。
encrypt = true 用于启用相应的服务端加密,Bucket 的私有访问、权限、版本保护仍需单独配置。备份文件也包含状态信息,不要提交到 Git。
迁移 backend 时保持受管资源配置不变。若为了迁移先把资源声明注释掉,后续 plan 可能把它理解为要求删除资源。terraform init -migrate-state 处理的是状态存储位置,资源删除是另一件事。
继续接上 K3s 和 Argo CD,会遇到什么?
demo_4 把流程拆成了 cvm.tf、k3s.tf 和 helm.tf。文件拆开之后,依赖仍然存在:
1 | 安全组和规则 |
课程中的 K3s 模块引用服务器的公网和私网 IP,再通过 SSH 安装。后面使用 remote-exec 在服务器上准备 kubeconfig,使用 local-exec 将它下载到执行 Terraform 的机器。
这段代码很适合解释 provisioner 的位置:remote-exec 在远端执行,local-exec 在本地执行。换到 CI runner 后,本地路径、SSH 密钥和工具是否存在,都要重新考虑。provisioner 通常伴随资源创建等生命周期事件执行,远端文件丢失也不会自动触发一次通用修复。
如果沿这条路线继续做,需要允许管理端访问 K3s API,并检查服务就绪和 kubeconfig。课程的具体 K3s 版本与模块来源属于当时的示例,复现前应核对兼容性和模块版本。
为什么加了 depends_on,Helm 仍可能失败?
课程的 Helm Provider 读取本地 config.yaml,这个文件却是在同一轮执行中稍后下载的。即使给 helm_release 加上资源依赖,Provider 在规划、刷新或初始化相关操作时,也可能已经需要读取文件或连接集群。
因此,把文件生成与 Provider 初始化挤在同一次执行里,容易产生先后矛盾。我更倾向于在练习里划分两个执行目录:
1 | infra/ # CVM、K3s,以及取得 kubeconfig |
先完成 infra,确认 kubeconfig 可用,再初始化 apps。下面是应用层的独立配置示例,使用课程中的 Helm 2.x Provider 语法:
1 | terraform { |
kubeconfig_path 传入已经存在的文件路径,Chart 版本选择与集群兼容的版本。上面没有包含 K3s 安装器,完整安装过程仍需结合 demo_4 检查,不能将这个应用层片段直接追加到前面的 CVM 实验后就认为集群已经存在。
应用部署后可以检查:
1 | kubectl --kubeconfig /path/to/config.yaml get nodes |
把基础设施层和应用层拆开,也意味着两个目录需要独立 state。两层的接口是集群访问配置,执行顺序则由人工或流水线明确安排。
dev 和 testing 怎样复用?
复制整个目录能快速得到第二套环境,但公共逻辑改动后就要修改多份代码。课件 demo_5 用模块解决这部分重复:
1 | environments/ |
cvm 模块输出公网和私网 IP,k3s 模块接收这些输出。下面是模块接口的连接片段,前提是模块已经声明对应输入和输出:
1 | module "cvm" { |
这里有两个值得检查的细节。课件中根模块声明了 region,但调用 CVM 模块时没有传下去;K3s 模块也有密码输入,调用处却依赖默认值。只修改根模块的地域或密码后,模块内部未必会跟着改变。
所以检查模块化代码时,我会沿着“根变量、模块参数、模块内部引用”逐段核对。把文件搬进 modules 目录,并不能保证输入已经正确接通。
上面省略了课程中的凭证参数,按前面的方式交给 Provider 从环境变量读取。整理成通用模块时,还可以在根模块配置 Provider,子模块声明所需 Provider 并继承调用者的配置,减少账号和地域配置散落在内部的问题。
dev 和 testing 各自配置输入和 backend 路径,不能只换一个实例名称却共用同一份状态。模块代码更新后,也需要分别进入各环境查看计划、决定何时应用;模块复用不会自动更新已经运行的环境。
如果已有资源从根模块迁入子模块,还要处理资源地址变化。应使用 moved 块或合适的状态迁移方式保持映射,先确认 plan 不会把“整理代码”解释成“删旧建新”。
实验结束,确认资源真的被清理
对于前面独立的 CVM 实验,可以先生成销毁计划:
1 | terraform plan -destroy -out=destroy.tfplan |
检查计划只包含这个目录管理的实验资源。已有 VPC、子网是输入参数,不在销毁范围内;独立准备的 COS Bucket 也应继续保留。
如果已经拆成 infra 和 apps,先在 apps 销毁应用,再到 infra 销毁集群和服务器。先删集群,Helm Provider 就可能无法访问 Kubernetes API,无法正常完成应用清理。
课件 README 中先用 state rm 移除 K3s 和 Argo CD 的记录,再销毁服务器。这会跳过相应对象的正常管理流程,不适合作为默认清理步骤。若发现销毁失败,先恢复必要的访问条件、检查依赖和远端对象,再决定如何处理状态。
最后到云控制台核对实验资源是否仍然存在,以及是否还残留需要付费的关联资源。state 为空只是一次状态检查,不能替代对云上环境的核对。
哪些文件放进 Git?
通常提交 .tf、必要的模块代码和 .terraform.lock.hcl。本地目录可以加入以下忽略规则:
1 | .terraform/ |
这份规则按文中的文件名编写。若换了计划文件或 kubeconfig 名称,要同步调整;用于分享的变量示例应使用占位值。真实 state、密码、私钥和可访问集群的配置都不属于公开博客的附件。
完成这一轮后,可以保留配置和一次经过脱敏的计划检查笔记:创建了哪些资源,改名影响了哪些字段,哪些对象复用已有资源,销毁时又检查了什么。下次增加 K3s 或新的模块时,就有一套可以对照的执行过程。
参考资料
- 课程代码:
lesson/module_2/demo_2、demo_3、demo_4、demo_5。 - 腾讯云 Provider
- COS backend
- Terraform provisioners
- Terraform 模块

