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 对照表
| Block | Label 数量 | 作用 | 是否必须 |
|---|---|---|---|
terraform | 0 | 声明 required_providers、backend 等 | 推荐 |
provider | 1 | 配置某个云厂商插件 | 视资源而定 |
resource | 2 | 声明被管理的资源 | 核心 |
data | 2 | 读取只读数据源 | 可选 |
variable | 1 | 输入参数 | 推荐 |
output | 1 | 输出值 | 推荐 |
locals | 0 | 局部值 | 可选 |
module | 1 | 调用模块 | 可选 |
一句话: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 / set | set 会丢失顺序 |
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 urlencode | user_data、签名 |
| 网络 | cidrhost cidrsubnet cidrnetmask | VPC 网段规划 |
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 是数据级循环,dynamicblock 用于块级循环;选错循环层级是 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 是如何被持久化与锁定的。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。