讲讲terraform的 module、workspace、remote backend、import 已有资源
Terraform 这几个概念这次可以对着你已经很熟悉的 Crossplane 概念来讲,因为 Terraform 是 IaC 领域另一条路线(声明式 + 自己维护一份 state),跟 Crossplane”声明式 CRD + K8s reconcile loop”解决的是同一类问题,但机制完全不同——很多地方能直接类比,能帮你更快建立直觉。
Module —— 可复用的配置单元
一个 Module 就是一个包含 .tf 文件的目录,可以被别的地方引用、传参调用,类似于”函数”。你写的每一份 Terraform 配置本身就是一个”根模块”(root module),可以在里面调用别的模块:
# modules/azure-rg/main.tf —— 定义一个模块
variable "name" {
type = string
}
variable "location" {
type = string
default = "eastasia"
}
resource "azurerm_resource_group" "this" {
name = var.name
location = var.location
}
output "id" {
value = azurerm_resource_group.this.id
}
# 根模块里调用它,可以调用多次,传不同参数
module "rg_dev" {
source = "./modules/azure-rg"
name = "example-dev-rg"
location = "eastasia"
}
module "rg_prod" {
source = "./modules/azure-rg"
name = "example-prod-rg"
location = "westus2"
}
source 可以是本地路径、Git 仓库地址、Terraform Registry 上的公共模块(比如 terraform-aws-modules/vpc/aws),也可以是 S3/Azure Blob 上打包好的版本化模块。核心价值是把”创建一个标准资源组 + 网络 + 权限”这套重复模式封装一次,之后每个环境只传几个变量就行,不用复制粘贴整段 HCL。
这跟 Crossplane 里的 Composition(把多个 Managed Resource 打包成一个可复用的 XRD/Composite Resource)本质上是同一个思路——只是 Terraform 的模块是”客户端渲染”(apply 时在本地/CI 里展开),Crossplane 的 Composition 是”服务端渲染”(在集群里由 Composition Controller 持续 reconcile)。
Workspace —— 同一份配置管理多套独立 state
terraform workspace new dev terraform workspace new prod terraform workspace select dev terraform apply
Workspace 让同一份 .tf 配置代码对应多个互相隔离的 state 文件——常见用法是同一套配置分别管 dev/staging/prod 三套环境,配置里用 ${terraform.workspace} 拿到当前选中的 workspace 名字,用来给资源加环境后缀:
resource "azurerm_resource_group" "this" {
name = "example-${terraform.workspace}-rg"
}
这里有个很容易踩的坑,得提前说清楚:CLI 的 terraform workspace 是个很轻量的机制,本质上只是”state 文件路径加了个前缀”,跟同一份 backend 配置、同一份变量文件共用——这意味着如果你忘了自己当前选的是哪个 workspace,terraform apply 完全可能把你以为是”dev”的改动应用到了 prod 上。正因为这个”炸弹半径”问题,很多团队实际生产里更倾向于用完全独立的目录 + 独立的 backend 配置来隔离环境,而不是用 workspace(一个目录一个环境,物理上不可能选错)。
(另外要提醒一句:Terraform Cloud/Enterprise 产品里也有个叫”Workspace”的东西,但那是完全不同、重得多的概念——包含独立的变量集、运行历史、VCS 集成,跟这里说的 CLI workspace 只是撞了同一个名字,别搞混。)
Remote Backend —— state 存在哪、怎么加锁
Terraform 的 state 是它自己维护的一份”我认为现在云上有什么”的记录——功能上跟 Crossplane 每个 Managed Resource CR 里的 status.atProvider 类似,但是 Crossplane 是分散存在每个 CR 对象里(存在 etcd 里),Terraform 是一整份集中的 JSON 文件(terraform.tfstate)。
默认情况下这个文件就存在你本地磁盘上,团队协作会有几个明显问题:没有锁(两个人同时 apply 会互相踩,state 文件损坏)、没法共享(新同事在自己电脑上跑,Terraform 完全不知道云上已经有资源了,会尝试重复创建)、文件里明文存着一些敏感字段。
Remote backend 就是把这份 state 挪到一个共享、带锁的存储上:
terraform {
backend "azurerm" {
resource_group_name = "example-rg"
storage_account_name = "tfstatestorage"
container_name = "tfstate"
key = "fleet-api.tfstate"
}
}
常见选择:AWS 上 s3 backend(配合 DynamoDB 表做加锁)、Azure 上 azurerm backend(Storage Account + Blob)、或者直接用 Terraform Cloud/Enterprise 托管(内置加锁、运行历史、policy-as-code)。改了 backend 配置之后要跑 terraform init -migrate-state 把已有 state 迁移过去。
这里的”锁”机制值得多说一句:这正好是 Crossplane/K8s 完全不需要操心的一类问题——K8s 的每次写操作天然经过 etcd 的 Raft 共识和乐观并发控制(resourceVersion 冲突检测),从设计上就不存在”两个人同时改坏一份中心文件”这种事;而 Terraform 的 state 是外部集中式文件,必须额外发明加锁机制来解决同一个问题。
Import —— 把已经存在的资源”收编”进 Terraform 管理
这个场景你其实已经很熟了——跟我们之前聊的”Crossplane 通过 ARM 的幂等 PUT 认领已有 resource group,而不是删除重建”是同一类需求:云上已经有一个资源(手动创建的,或者别的工具创建的),现在想让 IaC 工具接管它,但不能把它删了重建。
Terraform 有两种做法:
传统方式——先手写一个匹配的 resource 块,再用命令把它和真实资源的 ID 关联起来,只写入 state,不碰配置:
terraform import azurerm_resource_group.example \ /subscriptions/xxx/resourceGroups/example-rg
这里最大的坑:import 只是往 state 里塞了一条”这个地址对应这个云端 ID”的记录,但你配置文件里那个 resource 块的参数必须自己手写到跟云上现状完全匹配,否则下一次 terraform plan 会认为”配置想要的状态”和”实际状态”不一致,试图去”纠正”(哪怕这个纠正实际上是错的、你的配置才是写错的那一方,Terraform 并不知道)。所以 import 之后的铁律是:
terraform import azurerm_resource_group.example /subscriptions/.../example-rg terraform plan # 必须先看到 "No changes",才能确认配置跟现实完全对齐
如果这一步 plan 显示有 diff,说明是你的配置写得不对,得先把配置改到跟 diff 显示的”现状”一致,而不是直接 apply(否则可能把云上资源真实改坏,甚至某些字段改不了触发强制重建、直接删掉重建)。
较新的方式(Terraform 1.5+)安全得多,用声明式的 import 块,还能自动帮你生成配置:
# imports.tf
import {
to = azurerm_resource_group.example
id = "/subscriptions/xxx/resourceGroups/example-rg"
}
terraform plan -generate-config-out=generated.tf
这条命令会读取云上资源的真实属性,自动生成一份匹配的 resource 配置块写到 generated.tf 里,你再检查一下、挪到正式的 .tf 文件里就行——不用再自己一个字段一个字段对着 Azure Portal 手抄,也不容易出现”配置和现实不匹配”这种事故。
对比一下就能看出两边理念的差异:Crossplane 那边”认领 vs 重建”完全是隐式发生的——你只要 kubectl apply 一个声明相同 external-name 的 CR,ARM 的 PUT 语义自动帮你判断;Terraform 的 import 则是一次性的、显式的手动动作,之后 state 里就跟其他资源一样,没有”每次 apply 都重新判断认领”这回事——这也是两种工具在”声明式”这件事上路线不同的地方:K8s/Crossplane 是持续 reconcile(每隔一段时间自动核对现实),Terraform 默认只在你主动跑 plan/apply 的那一刻核对一次。
