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 更稳妥。

HCL 混合云模块:输入 → 推导 → 资源 → 产出 variable 输入 environment region vpc_cidr subnets (map) peer_vpc_id locals 推导 env_prefix az_count name_suffix subnet_list (for) 条件表达式 resource 资源建模 aws_vpc.this aws_subnet.this (for_each) aws_security_group.app dynamic "ingress" count = prod ? 1 : 0 output 产出契约 vpc_id / subnet_ids security_group_id dns_domain 混合云对端 私有云 VLAN 专线网关 本地 DNS 复用要点:变量声明类型与校验 → locals 集中推导 → for_each/dynamic 消除重复 → output 暴露稳定接口 → 上层模块按环境传参即可复用。 判断依据:terraform validate 通过、plan 中资源地址稳定、切换 environment 仅改变预期差异,即视为抽象合理。

四、把混合云资源抽象为可复用模块

模块化的核心不是“把文件拆开”,而是定义清晰的边界:哪些参数必须由调用方提供,哪些由模块内部推导,哪些结果必须暴露。一个典型的混合云网络模块目录结构如下:

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 与名称,不暴露内部实现细节,便于后续替换底层资源类型。
常见误区:在模块内部直接写死区域或账号 ID,会导致跨环境复用时必须改源码;把 count 与 for_each 混用于同一集合,容易在增删元素时触发资源重建。应优先用 for_each 加稳定键,把环境差异收敛到变量与 locals。

完成上述抽象后,可以用 terraform validate 检查语法与类型,用 terraform plan 观察不同 environment 下的资源地址与数量是否符合预期。若切换环境只产生预期的差异(如 prod 多一个 NAT 网关、多一条 443 规则),说明变量、locals、条件与循环的边界划分是合理的;若出现大量非预期重建,则应回到数据类型与 for_each 键的设计上排查。