Chapter 02

HCL 语法:资源、变量与输出

HCL 基础语法、resource/variable/output/locals 块、类型系统、tfvars 配置管理

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 使用稳定的字符串键,添加/删除某个键只影响对应的单个资源。

本章小结

本章核心要点