「HCL 语法与基础」

系统讲解 HCL 配置语言:block 结构与表达式体系、字符串与集合类型、内置函数、for 表达式与 dynamic block 动态语法、条件表达式,并给出常见编写避坑与风格建议。

1. HCL 概览与设计哲学

一句话总结: HCL 是声明式的 HashiCorp 配置语言,追求「人可读、机器可解析、与 JSON 互转」,是 Terraform 表达基础设施期望状态的唯一入口。

HCL(HashiCorp Configuration Language)不是传统编程语言,而是一种面向配置的声明式语言。写 HCL 时你不描述「如何做」,只描述「最终应该是什么样」,具体执行顺序由 Terraform 依赖图决定。这种心智模型是理解整个 Terraform 的基础。

# 声明式:描述期望状态,而非执行步骤
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
}

1.1 HCL 的两套语法

语法说明典型场景
Native Syntax.tf 文件,人类友好日常编写配置
JSON Syntax.tf.json 文件,程序生成机器生成、CI 工具

两条重要特性:表达式在 native 语法中可直接书写;HCL 原生语法与 JSON 可以无损互转(terraform fmt、hcl2json 工具辅助)。

1.2 HCL 与 JSON 的对应关系

HCL 原生语法可以无损转成 JSON 形态(.tf.json),block 名与 label 一一对应到嵌套的 JSON 对象,方便程序化生成与工具链解析;机器生成的配置通常直接用 JSON 语法,理解两者的等价关系有助于排查自动生成配置时的结构问题。

2. Block 结构与顶层块

一句话总结: HCL 中万物皆 block,Terraform 顶层块(provider/resource/data/variable/output/module/locals)各有职责,block 类型由「类型 + 一个或两个 label」共同确定。

HCL 的 block 语法为:类型 "label1" "label2" { ... }。Terraform 中 resource 使用两个 label(类型名 + 资源名),data 同样两个,而 variable 只有一个。

provider "aws" {
  region = var.region
}

variable "region" {
  type    = string
  default = "ap-northeast-1"
}

resource "aws_security_group" "web" {
  name = "web-sg"
}

locals {
  common_tags = { env = "prod" }
}

output "instance_ip" {
  value = aws_instance.web.public_ip
}

2.1 顶层 block 对照表

BlockLabel 数量作用是否必须
terraform0声明 required_providers、backend 等推荐
provider1配置某个云厂商插件视资源而定
resource2声明被管理的资源核心
data2读取只读数据源可选
variable1输入参数推荐
output1输出值推荐
locals0局部值可选
module1调用模块可选

一句话:block 的「类型 + label」组合构成 Terraform 的命名空间,同名同类型 block 会冲突,所以 resource "aws_instance" "a" 与 resource "aws_instance" "b" 是不同资源。

2.2 注释

# 单行注释(也支持 // 与 /* */ 块注释)
resource "aws_instance" "web" {
  ami = "ami-123456"   # 属性用 = 赋值,嵌套结构用 {} 表达
}

3. 表达式与类型系统

一句话总结: HCL 有字符串、数字、布尔、list、map、set、object、tuple 八种类型,一切表达式最终都归于这些类型,类型匹配错误是 HCL 最常见的运行期报错。

3.1 基本类型

locals {
  str   = "hello"                      # string
  num   = 42                           # number
  bool  = true                         # bool
  list  = ["a", "b", "c"]              # list(string)
  map   = { name = "web", env = "prod" }  # map(string)
  set   = toset(["x", "y", "x"])       # set,自动去重
  obj   = { id = 1, tags = ["a"] }     # object,各属性类型不同
  tuple = [1, "two", true]             # tuple,各元素类型不同
}
  • list 用 [],元素同类型;map 用 {},键值同类型
  • set 无序且去重,常用在 for_each 上
  • object 与 tuple 是「异构」集合,强调结构而非同质元素

3.2 类型约束与转换

variable "ingress_rules" {
  type = list(object({
    port        = number
    cidr_blocks = list(string)
  }))
  default = [
    { port = 80, cidr_blocks = ["0.0.0.0/0"] },
    { port = 443, cidr_blocks = ["0.0.0.0/0"] },
  ]
}

locals {
  # 显式转换
  port_str   = tostring(80)
  rule_set   = toset(var.ingress_rules[*].port)
  upper_map  = tomap({ a = "x" })
  keys_list  = keys({ a = 1, b = 2 })   # ["a", "b"]
}
转换函数作用注意事项
tostring数字/布尔转字符串布尔转成 “true”/“false”
tonumber字符串转数字非数字字符串会报错
tolist / toset转 list / setset 会丢失顺序
tomap转 map要求值同类型
keys / values取 map 键/值顺序不稳定,勿依赖

3.3 字符串插值与 heredoc

locals {
  name     = "web"
  instance = "${name}-ap-northeast-1"     # 插值:${...} 求值嵌入
  user_data = <<-EOT
    #!/bin/bash
    apt-get update
  EOT                                     # <<-EOT 去除公共缩进
}

<<-EOT 常用于 user_data、策略 JSON 等多行文本。

4. 内置函数

一句话总结: Terraform 内置数十个纯函数,核心集中在字符串、集合、映射、数字、编码五类,组合使用可以消灭大量手写代码。

4.1 字符串与编码函数

locals {
  joined    = join(",", ["a", "b", "c"])        # "a,b,c"
  splitted  = split(",", "a,b,c")                # ["a","b","c"]
  formatted = format("%s-%02d", "web", 3)        # "web-03"
  replaced  = replace("a-b-c", "-", "_")         # "a_b_c"
  upper     = upper("abc")                       # "ABC"
  encoded   = base64encode("hello")              # "aGVsbG8="
  sha       = sha256("secret")                   # 十六进制哈希
}

4.2 集合与映射函数

locals {
  merged   = merge({ a = 1 }, { b = 2 })         # {a=1,b=2}
  looked   = lookup({ a = 1 }, "x", 0)           # 键不存在时返回默认 0
  concated = concat([1, 2], [3, 4])              # [1,2,3,4]
  filtered = [for s in ["a", "bb", "c"] : s if length(s) > 1]  # ["bb"]
  lengths  = length(["a", "b"])                  # 2
  element  = element(["a", "b", "c"], 1)         # "b"
  distinct = distinct(["x", "y", "x"])           # ["x","y"]
  cidrs    = [for i in range(0, 4) : cidrhost("10.0.0.0/24", i)]  # 前 4 个 IP
}
类别常用函数典型用途
字符串join split format replace命名、拼接、格式化
集合length concat distinct element列表处理
映射merge lookup keys values合并标签、安全取默认值
编码base64encode sha256 urlencodeuser_data、签名
网络cidrhost cidrsubnet cidrnetmaskVPC 网段规划

4.3 容错函数 try 与 coalesce

locals {
  # try:表达式求值失败时回退,避免整个运行报错
  safe_value = try(data.aws_ami.latest.id, null)
  # coalesce:返回第一个非空值
  image_id = coalesce(var.custom_ami, data.aws_ami.latest.id, "ami-default")
}

try 只捕获求值错误,coalesce 用于「多候选取首个可用值」。

5. 动态表达式与循环

一句话总结: count 与 for_each 是资源级循环,for 表达式与 splat 是数据级循环,dynamic block 用于块级循环;选错循环层级是 HCL 最普遍的误用。

5.1 资源级循环 count 与 for_each

# count:基于整数,资源地址为 aws_instance.web[0..2]
resource "aws_instance" "web" {
  count         = 3
  ami           = "ami-123456"
  instance_type = "t3.micro"
}

# for_each:基于集合/map,资源地址为 aws_iam_user.users["alice"]
resource "aws_iam_user" "users" {
  for_each = toset(["alice", "bob", "carol"])
  name     = each.value
}

resource "aws_subnet" "azs" {
  for_each = {
    "a" = "10.0.1.0/24"
    "b" = "10.0.2.0/24"
  }
  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value
  availability_zone = "ap-northeast-1${each.key}"
}

each.key 与 each.value 只在 for_each 内可用;count.index 只在 count 内可用。

5.2 数据级循环 for 表达式与 splat

locals {
  users = {
    alice = "admin"
    bob   = "dev"
    carol = "dev"
  }
  # 过滤 + 转换:仅保留 dev,输出 "bob","carol"
  devs = [for name, role in users : name if role == "dev"]

  # map 形态的 for
  upper = { for name, role in users : name => upper(role) }

  # splat:取列表资源的所有属性
  all_ids = aws_instance.web[*].id
}

5.3 dynamic block 块级循环

resource "aws_security_group" "web" {
  name = "web-sg"

  dynamic "ingress" {                    # 块级循环
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = [ingress.value.cidr]
    }
  }
}

dynamic 内用 content {} 包裹,ingress.value 相当于 each.value。只有块(如 ingress、rules 等嵌套块)需要 dynamic,属性循环用 for 表达式即可。

6. 条件表达式与复杂组合

一句话总结: 三目条件 cond ? a : b 与逻辑运算符构成 HCL 的分支能力;「变量默认值 + 条件 + 函数」的组合可以写出几乎零 if 的声明式逻辑。

locals {
  # 条件表达式(var.environment 为 dev/staging/prod)
  is_prod     = var.environment == "prod"
  instance    = var.environment == "prod" ? "t3.large" : "t3.micro"
  replica_cnt = var.environment == "prod" ? 2 : 0

  # 逻辑运算
  enable_monitoring = var.environment == "prod" || var.enable_monitoring == true

  # 空值处理:null 显式赋值会绕过 default,用 coalesce 兜底
  zone = coalesce(var.zone, "ap-northeast-1a")
}
模式写法用途
环境差异prod ? x : y不同环境不同配置
安全默认coalesce(var.x, default)变量未传时兜底
空集合优雅降级length(var.list) > 0 ? var.list : ["default"]列表空时给默认
条件包含var.flag ? {a=1} : {}可选配置合并

6.1 把条件嵌套进对象合并

locals {
  base_tags = { project = "shop", owner = "team-infra" }
  # 生产环境额外打标
  tags = merge(
    base_tags,
    var.environment == "prod" ? { tier = "prod" } : {},
    var.environment == "dev" ? { tier = "dev" } : {},
  )
}

7. 常见 HCL 编写避坑

一句话总结: 循环对象用错、map 顺序依赖、隐式类型混用、try 掩盖真实错误,这四类是 HCL 运行期报错与「能跑但行为诡异」的高发区。

坑现象正确做法
for_each 用 list报错「for_each requires a map or set」先 toset(var.list)
依赖 map 的 keys 顺序资源随顺序漂移、diff 反复别依赖 map 顺序,用 set 表达无序
数字/字符串混用"1" 与 1 比较总是 false显式 tonumber/tostring
过度使用 try掩盖字段拼写错误仅对「可有可无的数据源」用 try
在 locals 里做副作用HCL 无副作用但隐藏意图locals 只做纯转换
用 count 控制可选资源count = 0 时索引访问报错用 try 或 length(count(...))>0 判断
# ❌ for_each 接 list 会报错 → ✅ 用 toset 转换
resource "aws_iam_user" "users" {
  for_each = toset(["alice", "bob"])
  name     = each.value
}

7.1 fmt 与校验

# 格式化:统一缩进与对齐,且会把 map 键对齐
terraform fmt

# 校验语法与类型
terraform validate

# 静态分析(第三方)
tflint

HCL 的报错信息通常带行号与块路径(如 aws_security_group.web),先看行号定位,再对照类型约束。

8. 总结

HCL 是 Terraform 的「通用语言」,掌握它等于掌握表达基础设施期望状态的语法能力:

环节要点
心智模型声明式,描述期望状态而非执行步骤
block类型 + label 构成命名空间,顶层块各司其职
类型八种类型,转换用 tostring/toset/tomap 等
函数join/lookup/merge/coalesce/try 覆盖大多数场景
循环count 与 for_each 管资源,for 与 splat 管数据,dynamic 管块
条件三目表达式 + merge 组合实现声明式分支
避坑for_each 配 toset、勿依赖 map 顺序、慎用 try

一句话收尾:先把 HCL 的 block 与表达式体系吃透,再看状态、模块、Provider 与流水线,整个 Terraform 专题就有了共同的语法地基。下一篇从「资源与状态管理」开始,理解声明式背后那份 tfstate 是如何被持久化与锁定的。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. 「漂移检测与收敛」
  2. 「资源重构与迁移」
  3. 「数据源与远程数据读取」