HCL 语言基础
什么是 HCL?
HCL(HashiCorp Configuration Language)是 HashiCorp 专为配置文件设计的语言。它的目标是兼具人类可读性和机器可解析性,比 JSON 更简洁,比 YAML 更强大,支持注释、函数调用、引用和复杂表达式。
块(Block)
HCL 的基本结构单位。由类型、可选标签和花括号包裹的内容组成,如 resource "aws_instance" "web" {}。
参数(Argument)
块内的键值对,如 instance_type = "t3.micro"。值可以是字符串、数字、布尔值、列表、映射或表达式。
表达式(Expression)
动态值,可以引用其他资源属性、变量、调用内置函数、使用条件和循环。
注释
单行注释用 # 或 //,多行注释用 /* */。
# 这是注释
// 这也是注释
/* 多行
注释 */
# resource 块:创建云资源
# 格式:resource "provider_type" "local_name" { ... }
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
# 嵌套块
tags = {
Name = "WebServer"
Env = "prod"
}
}
# 引用其他资源的属性:resource_type.local_name.attribute
output "instance_ip" {
value = aws_instance.web.public_ip
}
variable 定义详解
# variables.tf
# 基础变量(带类型约束)
variable "region" {
type = string
default = "us-east-1"
description = "AWS 区域"
}
# 数字类型
variable "instance_count" {
type = number
default = 1
validation {
condition = var.instance_count >= 1 && var.instance_count <= 10
error_message = "实例数量必须在 1-10 之间"
}
}
# 布尔类型
variable "enable_https" {
type = bool
default = true
}
# 列表类型
variable "availability_zones" {
type = list(string)
default = ["us-east-1a", "us-east-1b"]
}
# 映射类型
variable "instance_types" {
type = map(string)
default = {
dev = "t3.micro"
prod = "t3.large"
}
}
# 对象类型
variable "db_config" {
type = object({
engine = string
version = string
port = number
})
default = {
engine = "postgres"
version = "15.4"
port = 5432
}
}
# 敏感变量(值不会出现在 plan/apply 输出中)
variable "db_password" {
type = string
sensitive = true
}
terraform.tfvars 配置文件
# terraform.tfvars(自动加载)
region = "ap-northeast-1" # 东京
instance_count = 3
enable_https = true
instance_types = {
dev = "t3.small"
prod = "t3.xlarge"
}
# production.tfvars(通过 -var-file 指定)
# terraform apply -var-file="production.tfvars"
# 变量赋值优先级(从低到高):
# 1. 变量默认值
# 2. terraform.tfvars
# 3. *.auto.tfvars
# 4. -var-file 参数
# 5. -var 参数(命令行)
# 6. TF_VAR_xxx 环境变量
terraform apply -var="region=eu-west-1" -var="instance_count=5"
export TF_VAR_db_password="supersecret"
output 输出值
# outputs.tf
output "instance_id" {
value = aws_instance.web.id
description = "EC2 实例 ID"
}
output "load_balancer_dns" {
value = aws_lb.main.dns_name
description = "负载均衡器 DNS 名称"
}
# sensitive output(不显示在终端输出中)
output "db_connection_string" {
value = format("postgresql://%s:%s@%s:%d/%s",
aws_db_instance.main.username,
var.db_password,
aws_db_instance.main.endpoint,
aws_db_instance.main.port,
aws_db_instance.main.db_name
)
sensitive = true
}
locals 局部变量
# locals.tf — 避免重复,封装复杂逻辑
locals {
# 字符串拼接
name_prefix = "${var.project}-${var.environment}"
# 条件表达式
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# 公共标签(所有资源共享)
common_tags = {
Project = var.project
Environment = var.environment
ManagedBy = "Terraform"
CreatedAt = timestamp()
}
# 使用函数处理
bucket_name = lower(replace("${var.project}-${var.environment}-assets", "_", "-"))
}
# 使用 locals:local.name_prefix
resource "aws_instance" "web" {
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web"
})
}
variable vs locals 的选择
variable 用于接收外部输入(调用者可以赋值);locals 用于内部计算(外部不能直接修改,只能通过 variable 间接影响)。复杂的中间值计算用 locals,公开的配置接口用 variable。
HCL 类型系统完全参考
Terraform 的变量类型系统比看起来更丰富,理解类型系统有助于写出更严格、更安全的模块接口:
# 基础类型
variable "count" { type = number } # 整数或浮点数
variable "name" { type = string } # 字符串
variable "enabled" { type = bool } # true / false
variable "any_value" { type = any } # 不约束类型(谨慎使用)
# 集合类型
variable "regions" {
type = list(string) # 有序列表,允许重复:["us-east-1", "us-west-2"]
}
variable "subnet_map" {
type = map(string) # 字符串 map:{public="10.0.1.0/24", private="10.0.2.0/24"}
}
variable "unique_names" {
type = set(string) # 无序集合,自动去重:["alice", "bob"]
}
# 复合类型
variable "database_config" {
type = object({ # 固定结构的 map,每个字段有独立类型
host = string
port = number
name = string
ssl = bool
})
default = {
host = "localhost"
port = 5432
name = "mydb"
ssl = false
}
}
variable "server_list" {
type = list(object({ # 对象列表
name = string
instance_type = string
count = number
}))
default = [
{ name = "web", instance_type = "t3.medium", count = 3 },
{ name = "api", instance_type = "t3.large", count = 2 },
]
}
# tuple:固定长度、每个位置可以有不同类型(不常用)
variable "port_range" {
type = tuple([number, number])
default = [8080, 8443]
}
变量优先级顺序
Terraform 变量赋值有多个来源,优先级从低到高依次为:
变量赋值优先级(高者覆盖低者)
1. 变量的 default 值(最低优先级)
2. terraform.tfvars 文件
3. *.auto.tfvars 文件(按文件名字母顺序)
4. -var-file=filename.tfvars 命令行参数
5. -var="name=value" 命令行参数
6. TF_VAR_name 环境变量
7. -var 命令行直接赋值(最高优先级)
常见模式:
- terraform.tfvars 存放默认的开发配置
- prod.tfvars 存放生产配置,用 -var-file=prod.tfvars 指定
- CI/CD 通过环境变量 TF_VAR_xxx 注入密钥
依赖关系:隐式 vs 显式
# 隐式依赖:通过引用自动推断(推荐)
resource "aws_instance" "web" {
# 引用了 aws_subnet.public,Terraform 自动知道先创建 subnet 再创建 instance
subnet_id = aws_subnet.public.id
}
# 显式依赖:当逻辑依赖不能通过引用表达时
resource "null_resource" "setup" {
# 这个 null_resource 必须在 RDS 创建后才能运行
# 但它不直接引用 RDS 的属性
depends_on = [aws_db_instance.main]
provisioner "local-exec" {
command = "./scripts/setup-db.sh"
}
}
HCL 表达式与常用函数
Terraform 内置了丰富的函数,可以在任何表达式中调用,用于字符串处理、集合操作、类型转换和编码等。
字符串插值与多行字符串
locals {
# 字符串插值:使用 ${} 嵌入表达式
bucket_name = "${var.project}-${var.environment}-assets"
# 多行字符串(heredoc)
user_data = <<-EOF
#!/bin/bash
export ENV=${var.environment}
yum install -y nginx
systemctl start nginx
EOF
# 条件表达式:condition ? true_val : false_val
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# 嵌套条件(可读性差,推荐用 lookup 或 map 替代)
size = var.environment == "prod" ? "large" : (
var.environment == "staging" ? "medium" : "small"
)
# 推荐:用 map lookup 代替多层嵌套条件
size_map = {
prod = "t3.large"
staging = "t3.medium"
dev = "t3.micro"
}
instance_size = lookup(local.size_map, var.environment, "t3.micro")
}
常用内置函数
locals {
# ===== 字符串函数 =====
upper_env = upper(var.environment) # "PROD"
lower_name = lower(var.project) # "my-project"
trimmed = trimspace(" hello ") # "hello"
replaced = replace("a_b_c", "_", "-") # "a-b-c"
formatted = format("%-10s: %d", "count", 5) # "count : 5"
joined = join(",", ["a", "b", "c"]) # "a,b,c"
split_list = split(",", "a,b,c") # ["a","b","c"]
contains_x = contains(["a", "b"], "a") # true
# ===== 集合函数 =====
item_count = length(["a", "b", "c"]) # 3
first_item = element(["a", "b"], 0) # "a"(支持负数索引)
flat_list = flatten([["a"], ["b", "c"]]) # ["a","b","c"]
unique_list = distinct(["a", "b", "a"]) # ["a","b"](去重)
sliced = slice(["a", "b", "c"], 1, 3) # ["b","c"](左闭右开)
sorted = sort(["c", "a", "b"]) # ["a","b","c"]
reversed = reverse(["a", "b", "c"]) # ["c","b","a"]
# map 函数
merged_map = merge({a=1}, {b=2}, {c=3}) # {a=1, b=2, c=3}(后者覆盖前者)
map_keys = keys({a=1, b=2}) # ["a","b"]
map_values = values({a=1, b=2}) # [1, 2]
map_lookup = lookup({a="x"}, "a", "default") # "x"(键不存在返回默认值)
# ===== 类型转换函数 =====
to_num = tonumber("42") # 42
to_str = tostring(42) # "42"
to_set = toset(["a", "b", "a"]) # {"a","b"}(list 转 set,去重)
to_list = tolist({"a", "b"}) # ["a","b"](set 转 list)
# ===== 编码函数 =====
b64_encoded = base64encode("hello world") # "aGVsbG8gd29ybGQ="
b64_decoded = base64decode("aGVsbG8gd29ybGQ=") # "hello world"
json_str = jsonencode({a = 1, b = "two"}) # '{"a":1,"b":"two"}'
json_obj = jsondecode('{"key":"value"}') # {key="value"}
# ===== 文件函数 =====
ssh_pubkey = file("~/.ssh/id_rsa.pub") # 读取文件内容为字符串
tpl_result = templatefile("userdata.tpl", { # 渲染模板文件(替换变量)
env = var.environment
host = aws_instance.web.private_ip
})
# ===== 其他常用函数 =====
max_val = max(3, 1, 4, 1, 5) # 5
min_val = min(3, 1, 4) # 1
abs_val = abs(-5) # 5
floor_val = floor(3.7) # 3
ceil_val = ceil(3.2) # 4
}
for 表达式:集合变换
locals {
# list comprehension:过滤和转换列表
subnet_ids = ["subnet-001", "subnet-002", "subnet-003"]
# 将所有 ID 转为大写
upper_ids = [for id in local.subnet_ids : upper(id)]
# 过滤只保留以 "subnet-00" 开头的
filtered = [for id in local.subnet_ids : id if startswith(id, "subnet-00")]
# map comprehension:转换 map
ports = { http = 80, https = 443, ssh = 22 }
# 值加 1000(模拟映射非标准端口)
high_ports = { for name, port in local.ports : name => port + 1000 }
# list → map:将列表转换为以某字段为 key 的 map(用于 for_each)
servers = [
{ name = "web", type = "t3.micro" },
{ name = "api", type = "t3.small" },
]
servers_map = { for s in local.servers : s.name => s }
# 结果:{ web = {name="web", type="t3.micro"}, api = {...} }
# map → list:提取 map 的值组成列表
type_list = [for _, s in local.servers_map : s.type]
# 结果:["t3.micro", "t3.small"]
}
Resource 的 lifecycle 元参数
每个 resource 块都可以包含 lifecycle 子块,控制 Terraform 如何管理资源的生命周期:
resource "aws_db_instance" "main" {
identifier = "production-db"
# ...
lifecycle {
# create_before_destroy:先创建新资源再销毁旧资源
# 默认行为是先销毁再创建(对于需要强制替换的资源,这意味着短暂停机)
# 设为 true 可实现零停机替换(新资源 ID 生成后再删旧的)
create_before_destroy = true
# prevent_destroy:防止意外删除重要资源
# 若 plan 中包含此资源的 destroy,terraform apply 会直接报错
# 注意:terraform destroy 也会被阻止
prevent_destroy = true
# ignore_changes:忽略某些属性的变化(不将手工改动同步回来)
# 场景:Auto Scaling 会自动修改 desired_capacity,我们不想 Terraform 覆盖它
ignore_changes = [
tags, # 忽略标签变化(手工打标签后不被覆盖)
desired_capacity, # 忽略 ASG 期望容量变化
]
# replace_triggered_by:当指定资源/属性变化时,触发此资源的替换
# 场景:SSL 证书更新后需要重建 LB 监听器
replace_triggered_by = [
aws_acm_certificate.main.id
]
}
}
# ignore_changes 特殊用法:忽略所有属性变化
# 适合完全由外部管理的资源(Terraform 只创建,不管后续改动)
resource "aws_instance" "app" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
ignore_changes = all # 创建后不再追踪任何属性变化
}
}
count 和 for_each:创建多个资源
# ===== count:数字索引 =====
# 适合创建多个完全相同(或仅序号不同)的资源
variable "instance_count" {
type = number
default = 3
}
resource "aws_instance" "web" {
count = var.instance_count # 创建 3 个实例
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
# count.index:当前实例的序号(从 0 开始)
tags = { Name = "web-${count.index + 1}" }
}
# 引用特定实例:aws_instance.web[0].id
# 引用所有实例 ID 列表:aws_instance.web[*].id
# count = 0:条件创建资源(等价于不创建)
resource "aws_cloudwatch_alarm" "high_cpu" {
count = var.enable_monitoring ? 1 : 0
# ...
}
# ===== for_each:Map/Set 键索引 =====
# 适合创建多个配置不同的资源,且需要稳定的标识符
# 好处:添加/删除某个条目只影响该条目对应的资源,不影响其他资源
# count 的缺点:删除中间的元素会导致后续所有索引重排,触发意外重建
variable "s3_buckets" {
type = map(object({
versioning = bool
region = string
}))
default = {
assets = { versioning = false, region = "us-east-1" }
logs = { versioning = true, region = "us-east-1" }
backups = { versioning = true, region = "us-west-2" }
}
}
resource "aws_s3_bucket" "buckets" {
for_each = var.s3_buckets # each.key = "assets"/"logs"/"backups"
bucket = "my-${each.key}-bucket"
# each.value.versioning, each.value.region
tags = { Name = each.key }
}
# 引用特定实例:aws_s3_bucket.buckets["logs"].id
# 引用所有 ID 的 map:{ for k,v in aws_s3_bucket.buckets : k => v.id }
# for_each + toset(用于简单字符串列表)
resource "aws_iam_group_membership" "admins" {
for_each = toset(var.admin_users) # 将列表转为集合,去重并用作 key
name = "admin-${each.value}" # for set 时,each.key == each.value
# ...
}
count vs for_each 的选择
优先使用 for_each,除非资源确实是完全同质的(如指定数量的完全相同的节点)。count 的问题:删除或插入中间的元素会导致后续索引重排,Terraform 会认为这些资源需要更新或重建。for_each 使用稳定的字符串键,添加/删除某个键只影响对应的单个资源。
本章小结
本章核心要点
- HCL 四种基本块:resource(创建资源)、variable(接收外部输入)、output(暴露值)、locals(内部计算);provider 和 terraform 块用于全局配置。
- 资源引用语法:
resource_type.resource_name.attribute;引用会自动建立隐式依赖关系,Terraform 据此确定创建顺序。 - 变量类型系统:string/number/bool 是基础类型;list/map/set 是集合类型;object/tuple 是结构化类型;类型约束让模块接口更严格。
- 变量优先级:default < tfvars < auto.tfvars < -var-file < TF_VAR_xxx < -var;生产中通过 CI 环境变量注入敏感值。
- locals vs variable:variable 是外部输入接口;locals 是内部计算结果,不能从外部直接赋值。
- depends_on:当资源间存在逻辑依赖但没有直接属性引用时,用显式 depends_on 声明;尽量优先用隐式依赖(通过引用)。