3. HCL语法与资源建模
在混合云场景中,同一套业务往往要同时落到公有云 VPC、私有云 VLAN、专线网关和本地 DNS 上。如果每个环境都手写一遍资源,很快就会陷入“改一处、漏三处”的困境。HCL 的价值就在于:把“哪些是输入、哪些是推导、哪些是产出、哪些是重复结构”用语言本身表达清楚,从而把混合云资源抽象成可复用模块。本节围绕变量、局部值、输出、数据类型与表达式、动态块、条件与循环,以及模块化封装展开。
一、输入、推导与产出:变量、局部值、输出
变量(variable)是模块的输入契约。混合云模块通常需要 region、cidr_block、environment、peer_vpc_id 等参数。为变量声明类型和校验,可以把错误挡在计划阶段:
variable "environment" {
type = string
description = "部署环境标识"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment 只能是 dev、staging 或 prod。"
}
}
variable "vpc_cidr" {
type = string
default = "10.20.0.0/16"
}
局部值(locals)用于承载“由输入推导出来、但不需要外部传入”的中间结果。例如根据环境决定网段前缀、根据区域决定可用区数量:
locals {
env_prefix = var.environment == "prod" ? "prd" : "dev"
az_count = var.environment == "prod" ? 3 : 2
subnet_bits = 8
name_suffix = "${local.env_prefix}-${var.region}"
}
输出(output)是模块的产出契约,供上层模块或根模块引用。混合云模块至少应输出 VPC ID、子网 ID 列表、安全组 ID 和 DNS 域名,便于对端网络与本地 DNS 对接:
output "vpc_id" {
value = aws_vpc.this.id
description = "本模块创建的 VPC ID"
}
output "subnet_ids" {
value = aws_subnet.this[*].id
}
二、数据类型与表达式:让推导可读可校验
HCL 的基础类型包括 string、number、bool,复合类型包括 list、set、map、object 和 tuple。混合云模块中,用 object 描述“每个子网的名称、网段、可用区”比用多个平行列表更安全:
variable "subnets" {
type = map(object({
cidr = string
az = string
public = bool
}))
default = {
web = { cidr = "10.20.1.0/24", az = "cn-north-1a", public = true }
app = { cidr = "10.20.2.0/24", az = "cn-north-1b", public = false }
}
}
表达式方面,条件表达式 cond ? a : b 用于按环境切换取值;for 表达式用于把 map 转成 list 或做投影;lookup、merge、flatten、try 用于处理可选字段与嵌套结构。例如把子网 map 展开为带标签的列表:
locals {
subnet_list = [
for name, cfg in var.subnets : {
name = "${local.name_suffix}-${name}"
cidr = cfg.cidr
az = cfg.az
public = cfg.public
}
]
}
三、动态块、条件与循环:消除重复结构
当资源内部存在数量不定的嵌套块时,dynamic 块可以避免复制粘贴。例如安全组规则数量随环境变化:
resource "aws_security_group" "app" {
name = "${local.name_suffix}-app"
vpc_id = aws_vpc.this.id
dynamic "ingress" {
for_each = var.environment == "prod" ? [80, 443] : [80]
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
条件与循环还体现在资源级:count 适合“要么 0 个、要么 N 个”的场景,例如仅生产环境创建 NAT 网关;for_each 适合按 map 键稳定寻址,避免删除中间元素导致索引漂移。混合云中跨云对等连接、路由表条目通常用 for_each 更稳妥。
四、把混合云资源抽象为可复用模块
模块化的核心不是“把文件拆开”,而是定义清晰的边界:哪些参数必须由调用方提供,哪些由模块内部推导,哪些结果必须暴露。一个典型的混合云网络模块目录结构如下:
modules/hybrid-network/
├── main.tf # VPC、子网、路由表、安全组
├── variables.tf # 输入契约
├── locals.tf # 命名、网段、AZ 推导
├── outputs.tf # 对外接口
└── versions.tf # provider 与 terraform 版本约束
调用方只需传入环境与网段,即可在公有云与私有云两侧复用同一模块:
module "network_prod" {
source = "./modules/hybrid-network"
environment = "prod"
region = "cn-north-1"
vpc_cidr = "10.20.0.0/16"
subnets = {
web = { cidr = "10.20.1.0/24", az = "cn-north-1a", public = true }
app = { cidr = "10.20.2.0/24", az = "cn-north-1b", public = false }
}
}
object 而非多个平行 list,避免“第 3 个网段配第 2 个可用区”这类错位;输出只暴露稳定 ID 与名称,不暴露内部实现细节,便于后续替换底层资源类型。count 与 for_each 混用于同一集合,容易在增删元素时触发资源重建。应优先用 for_each 加稳定键,把环境差异收敛到变量与 locals。完成上述抽象后,可以用 terraform validate 检查语法与类型,用 terraform plan 观察不同 environment 下的资源地址与数量是否符合预期。若切换环境只产生预期的差异(如 prod 多一个 NAT 网关、多一条 443 规则),说明变量、locals、条件与循环的边界划分是合理的;若出现大量非预期重建,则应回到数据类型与 for_each 键的设计上排查。