4. 基础设施即代码工程化起步

当 Terraform 从“一个人写几个资源”进入“团队协作维护多云基础设施”阶段,最先暴露的问题往往不是语法,而是工程结构。所有资源堆在一个 main.tf 里,变量散落在各处,输出没有统一约定,环境之间靠复制粘贴区分——这样的代码无法复用,也无法安全演进。本节要解决的核心问题是:如何用模块化设计、清晰的目录结构、规范的变量与输出管理,构建一套可复用的多云基础模板。

一、模块化设计:把“能跑”变成“可复用”

Terraform 模块本质上是一组被打包的 .tf 文件集合,它对外暴露输入变量和输出值,对内封装资源实现。模块化的判断标准很简单:如果一段资源配置需要在两个以上地方出现,或者它对应一个清晰的业务能力(如“一个具备公网访问能力的 VPC”“一套标准对象存储桶”),就应该抽成模块。

以多云场景为例,我们通常按“能力”而非“云厂商”划分模块。例如 network 模块负责 VPC/VNet、子网、路由表、安全组;storage 模块负责对象存储与生命周期策略;compute 模块负责虚拟机或容器集群。每个模块内部再通过 provider 别名或条件判断适配 AWS、Azure、阿里云等不同后端。这样上层调用者只关心“我要一个网络”,而不关心底层是哪个云。

模块的目录结构建议采用如下约定:

modules/
  network/
    main.tf        # 资源定义
    variables.tf   # 输入变量声明
    outputs.tf     # 输出值声明
    versions.tf    # provider 与 terraform 版本约束
    README.md      # 模块用途、输入输出说明
  storage/
    ...
environments/
  dev/
    main.tf        # 调用模块,传入 dev 参数
    terraform.tfvars
  prod/
    main.tf
    terraform.tfvars

这种“模块层 + 环境层”分离的结构,使模块保持稳定,环境层只负责组合与参数注入。模块内部不应硬编码任何环境相关值,所有可变部分必须通过变量传入。

二、变量管理:类型、默认值与校验

变量是模块的接口。一个可复用的模块,其变量设计应当满足三点:类型明确、默认值合理、必要时可校验。Terraform 支持 string、number、bool、list、map、object 等类型,复杂结构优先使用 object 以获得更好的可读性和校验能力。

以下是一个网络模块的变量声明示例,展示了类型约束与校验规则:

variable "environment" {
  type        = string
  description = "环境标识,如 dev / staging / prod"
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment 必须是 dev、staging 或 prod 之一。"
  }
}

variable "vpc_cidr" {
  type        = string
  default     = "10.0.0.0/16"
  description = "VPC 网段,需为合法 CIDR"
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "vpc_cidr 必须是合法 CIDR,例如 10.0.0.0/16。"
  }
}

variable "subnets" {
  type = list(object({
    name   = string
    cidr   = string
    public = bool
  }))
  description = "子网列表,public 决定是否关联公网路由"
}

变量值的来源优先级从低到高依次为:默认值、terraform.tfvars、*.auto.tfvars、命令行 -var、环境变量 TF_VAR_。在团队协作中,建议将非敏感值写入 terraform.tfvars 并纳入版本控制,敏感值(如访问密钥)通过环境变量或密钥管理服务注入,绝不写入代码仓库。

三、输出管理:让模块之间可组合

输出值(output)是模块对外的返回值,也是模块组合的桥梁。一个模块的输出应当只暴露上层真正需要的属性,而不是把所有资源属性都抛出去。例如网络模块应输出 vpc_id、subnet_ids、security_group_id,计算模块再通过 module.network.vpc_id 引用。

output "vpc_id" {
  value       = aws_vpc.this.id
  description = "VPC 唯一标识,供计算与存储模块引用"
}

output "subnet_ids" {
  value       = { for s in aws_subnet.this : s.tags.Name => s.id }
  description = "子网名称到 ID 的映射"
}

输出同样支持 sensitive = true 标记,用于避免密钥、连接串等在 terraform output 中明文显示。对于跨环境共享的值,还可以使用 terraform_remote_state 数据源读取其他状态文件的输出,但更推荐通过模块直接组合,减少隐式依赖。

四、多云基础模板的整体数据流

下图展示了本节所构建模板的核心结构:环境层通过变量向模块层注入参数,模块层内部封装各云厂商资源,最终通过输出值向上返回关键标识,供其他模块或环境引用。图中同时标出了变量校验与敏感值处理的位置。

环境层 environments/ dev/main.tf module "network" { ... } prod/main.tf module "network" { ... } terraform.tfvars environment = "dev" vpc_cidr = "10.0.0.0/16" 敏感值注入 TF_VAR_access_key (不写入仓库) 变量校验 validation { condition } 类型约束 object/list 模块层 modules/ network/ VPC / Subnet / SG variables.tf · outputs.tf versions.tf storage/ Bucket / Lifecycle variables.tf · outputs.tf versions.tf compute/ VM / Cluster variables.tf · outputs.tf versions.tf 多云适配 provider 别名 条件判断 / count 统一接口 输出层 outputs vpc_id 供 compute 模块引用 module.network.vpc_id subnet_ids 名称到 ID 的映射 for 表达式生成 bucket_arn 存储模块输出 sensitive = false sensitive 输出 db_password sensitive = true terraform output 查看 引用 环境层注入参数 → 模块层封装资源 → 输出层返回标识,形成可组合的多云基础模板

五、可复用模板的落地检查

在把模板交给团队使用前,建议逐项确认:模块是否只通过变量接收环境差异;变量是否都有类型和描述;敏感变量是否已标记 sensitive 或通过环境变量注入;输出是否只暴露必要属性;versions.tf 是否锁定了 Terraform 与 provider 的版本范围。这些检查能显著降低后续多云扩展时的返工成本。

模块版本建议使用 Git tag 管理,环境层通过 source = "git::https://...?ref=v1.2.0" 引用固定版本,避免上游模块变更直接冲击生产环境。

完成本节后,你应当能够独立搭建一个“环境层调用模块层、模块层封装多云资源、输出层提供组合接口”的基础模板。下一阶段的多云迁移工作,都将在这个结构之上展开。