讲讲terraform的 module、workspace、remote backend、import 已有资源
这四个是 Terraform 实际项目里绕不开的核心机制,分别解决”怎么复用配置””怎么隔离环境””state 存哪””怎么接管已经存在的资源”这四个问题,我逐个讲。
Module:代码复用和封装
Module 就是一组 Terraform 配置文件的集合,可以被别的配置引用、传参、复用,概念上类似编程语言里的函数或者类。你写的任何一个 Terraform 目录本身就是一个 module(叫 root module),你可以把常用的资源组合(比如”一个标准的 VPC + 子网 + 路由表”)封装成一个 child module,以后每次要新建一套网络,不用重新写一遍资源定义,直接引用这个 module 并传不同的参数即可:
module "network" {
source = "./modules/network" # 也可以是 registry、git 仓库地址
version = "1.2.0" # 引用 Registry 上的模块时可以锁版本
vpc_cidr = "10.0.0.0/16"
environment = "prod"
}
Module 内部通过 variable 声明输入参数,通过 output 把内部创建的资源属性暴露出去供外部引用(比如把 VPC ID 传给别的 module)。好处很直接:统一标准、减少复制粘贴、把最佳实践沉淀成可版本化的组件,团队协作时不同的人写不同 module、组合使用即可。公开的 Terraform Registry(registry.terraform.io)上有大量官方和社区维护的现成 module,可以直接拿来用,不用所有东西都自己造轮子。
Workspace:同一份配置管理多套 state
Workspace 是让同一套 Terraform 配置对应多份独立 state 文件的机制,典型场景是同一套基础设施代码,想分别管理 dev/staging/prod 三套环境:
terraform workspace new dev terraform workspace new prod terraform workspace select dev terraform apply
每个 workspace 有自己独立的 state,互不影响,配置文件里可以用 terraform.workspace 这个内置变量拿到当前 workspace 名字,动态调整资源命名或参数:
resource "aws_instance" "app" {
tags = {
Name = "app-${terraform.workspace}"
}
instance_type = terraform.workspace == "prod" ? "m5.large" : "t3.micro"
}
需要提醒的是,workspace 是一种相对轻量的隔离机制,很多团队实际生产中会避开它,转而用”每个环境一个独立目录 + 独立 backend 配置”的方式,原因是 workspace 切换靠命令行操作,容易出现”忘了切换到哪个 workspace 就手滑 apply”的事故,而且所有 workspace 共享同一份变量文件和 provider 配置,想给不同环境用不同的云账号/权限就比较别扭。目录隔离虽然要多写点重复配置,但环境边界更硬,出错代价更低,是很多经验团队更倾向的做法。
Remote backend:我们最早聊过的 state 存储问题的落地方案
这个我们第一次聊 state 的时候就提到过原理,这里补充一些实操细节。backend 配置写在 terraform block 里:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks" # 用于加锁
encrypt = true
}
}
几个实操要点:backend 配置不支持用变量插值(没法引用 var.xxx),因为 backend 初始化发生在其他一切之前,这时候变量系统还没建立,所以环境相关的 key 路径通常靠 -backend-config 参数在 terraform init 时传入,或者干脆用前面说的目录隔离,每个目录写死自己的 backend 配置。如果要更换 backend(比如从本地迁移到 S3,或者换一个 bucket),执行 terraform init -migrate-state,Terraform 会引导你把现有 state 内容迁移过去,而不是丢弃重来。团队协作中 remote backend 加上锁机制(S3+DynamoDB、Azure Storage 的 blob lease、Terraform Cloud 内置锁)是防止并发 apply 冲突的硬性要求,单人玩具项目可以用本地 state,团队项目基本没有不用远程 backend 的。
Import:把已经存在、但不是 Terraform 创建的资源”收编”进来
现实中经常遇到这种情况:某个资源是别人手动在控制台创建的,或者是历史遗留、用别的工具创建的,现在想让 Terraform 接管它的管理权,这时候就需要 import。早期版本只有命令行方式:
terraform import aws_s3_bucket.my_bucket my-existing-bucket-name
这条命令只会把资源”塞进” state 文件里,让 Terraform 知道”这个 state 地址对应云上这个实际资源”,但不会帮你生成对应的 HCL 配置代码——你必须自己手写一份 resource "aws_s3_bucket" "my_bucket" {...} 配置,并且要保证配置里的参数和资源当前实际状态基本吻合,否则 import 完之后立刻跑一次 plan 会看到一堆意料之外的变更(因为配置和真实状态对不上)。这是早期 import 最麻烦的地方——纯靠人工核对属性,容易漏、容易错。
Terraform 1.5 之后引入了声明式的 import block,加上 -generate-config-out 参数,能大幅简化这个过程:
import {
to = aws_s3_bucket.my_bucket
id = "my-existing-bucket-name"
}
跑 terraform plan -generate-config-out=generated.tf,Terraform 会自动帮你把这个资源当前的真实属性反向生成一份 HCL 配置文件,你只需要检查、微调一下(比如把一些没必要写死的字段改成变量引用),而不用从零手写,大大降低了 import 的出错率。
四者放在一起怎么用
一个相对成熟的项目结构大概是:用 module 封装可复用的资源组合,不同环境用独立目录(或谨慎使用 workspace)分别管理,每个环境的 state 存在各自的 remote backend 里并加锁,遇到历史遗留资源用 import(优先用声明式 import block + 自动生成配置)把它们逐步纳入统一管理。这四个机制分别解决”复用””隔离””存储””接管”这四个不同维度的问题,是团队协作场景下 Terraform 项目能跑得稳的基本功。
