HCL 基础
本章目标:掌握 Terraform 配置语言(HCL)的核心构件——resource / data / variable / output / locals,以及表达式、count / for_each、dynamic 等常用手法。这是后面所有章节的地基。
完成后你应能写出一份「可 validate、可读、可复用」的 main.tf。
1. 一个最小配置长什么样
terraform {
required_version = ">= 1.5.0, < 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.40"
}
}
}
provider "aws" {
region = "ap-guangzhou"
}
resource "aws_instance" "web" {
ami = "ami-0abcd1234"
instance_type = "t3.small"
tags = {
Name = "web-01"
}
}
四个顶层块:
| 块 | 作用 |
|---|---|
terraform {} | 版本、provider 约束、backend |
provider "aws" {} | 云厂商连接配置(地域、凭证来源等) |
resource "类型" "名字" {} | 声明一个真实资源——这是 Terraform 的主角 |
variable {} / output {} / locals {} | 输入 / 输出 / 局部值(见后) |
引用语法统一:类型.名字.属性,例如 aws_instance.web.id、var.region、local.base_tags。
2. resource:声明真实资源
resource "aws_security_group" "web" {
name = "web-sg"
description = "HTTP/HTTPS for web tier"
vpc_id = "vpc-0abc123"
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "web-sg"
}
}
ingress 这种没有名字、可重复的写法叫 block argument(嵌套块),和 tags = {} 这种 key = value 的普通参数不同。看文档时要区分「是参数还是嵌套块」。
2.1 Meta-arguments(资源特有的保留参数)
写在任意 resource 里都生效:
| Meta-argument | 作用 |
|---|---|
count | 复制 N 份,count.index 取序号 |
for_each | 按 map/set 复制,each.key / each.value 取值 |
depends_on | 显式声明依赖(一般无需,Terraform 会自动推断) |
provider | 指定用哪个 provider 别名 |
lifecycle | 控制创建/替换/忽略行为(第 8 章细讲) |
resource "aws_instance" "web" {
count = 3
ami = "ami-0abcd1234"
instance_type = "t3.small"
tags = {
Name = "web-${count.index + 1}"
}
}
# 引用第 2 台:aws_instance.web[1].id
count用列表序号,增删中间项会导致后续序号全部重排、资源被重建——危险。for_each用稳定 key(如 map 的键),增删一项不影响其他项,生产首选。
3. data:读取已有资源,不创建
data 只查询已有对象,不产生变更,常用于「引用别人/上一套 Terraform 建好的东西」:
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # 引用查询结果
instance_type = "t3.small"
}
典型用途:查最新 AMI、读现有 VPC/子网、拉取账号信息(data.aws_caller_identity)。
4. variable:输入参数
把「会变的东西」抽成变量,配置才复用得了。
variable "instance_type" {
description = "EC2 机型"
type = string
default = "t3.small"
validation {
condition = can(regex("^t3\\.", var.instance_type))
error_message = "只接受 t3 系列机型。"
}
}
variable "subnet_ids" {
description = "子网列表"
type = list(string)
default = []
}
variable "db_password" {
description = "数据库密码"
type = string
sensitive = true # 在 plan/输出的展示中打码(注意:state 文件里仍是明文,见第 5 章)
}
| 字段 | 作用 |
|---|---|
type | string / number / bool / list(...) / map(...) / set(...) / object({...}) / any |
default | 不传则用之;无 default 则该变量必填 |
validation {} | 自定义约束,报错信息要面向人 |
sensitive | 控制台/输出中打码(不改变 state 存储方式) |
nullable | 是否允许 null(默认 true) |
传值方式:terraform.tfvars、*.auto.tfvars、-var、-var-file,或用环境变量 TF_VAR_<name>。优先级:-var > *.auto.tfvars > terraform.tfvars。
5. output:导出结果
供人看,也供其他模块(或 terraform state output)读取:
output "web_private_ips" {
description = "web 节点的内网 IP"
value = aws_instance.web[*].private_ip
}
output "db_password" {
description = "请勿外泄"
value = var.db_password
sensitive = true
}
6. locals:局部计算值
把一段复杂表达式命名一下,避免重复、提升可读性:
locals {
env = "staging"
base_tags = {
Environment = local.env
ManagedBy = "terraform"
Owner = "platform-team"
}
# 合并基础标签与资源特有标签
web_tags = merge(local.base_tags, { Service = "web" })
}
resource "aws_instance" "web" {
# ...
tags = local.web_tags
}
locals 只在当前模块内可见,不能从外部传值,也不能作为模块输入/输出。它纯粹是「模块内部的局部变量」。模块间传值用 variable / output。
7. 表达式、函数与循环
7.1 插值与函数
name = "web-${var.env}-${count.index}"
bucket = "app-${lower(var.env)}-logs"
subnet = cidrsubnet("10.0.0.0/16", 8, 3) # 10.0.3.0/24
tags = merge(var.tags, { Extra = "x" })
first = element(var.subnets, 0)
val = lookup(var.config, "key", "default")
safe = try(var.maybe_null.attr, "fallback")
常用内置函数:cidrsubnet / cidrhost、merge / zipmap、element / slice、lower / upper / trimspace、templatefile、try / can、length / toset / keys / values。
7.2 条件表达式
instance_count = var.env == "prod" ? 3 : 1
7.3 for_each 实战
variable "users" {
type = map(string) # { alice = "alice@example.com", bob = "bob@example.com" }
default = {}
}
resource "aws_iam_user" "this" {
for_each = var.users
name = each.key
tags = {
Email = each.value
}
}
# 引用:aws_iam_user.this["alice"].arn
for_each 的值必须是 map 或 set of strings,且 key 必须可预测、稳定。
7.4 dynamic 块:按需生成嵌套块
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
cidr_blocks = list(string)
}))
default = []
}
resource "aws_security_group" "web" {
# ...
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = "tcp"
cidr_blocks = ingress.value.cidr_blocks
}
}
}
7.5 多行字符串(heredoc)
user_data = <<-EOT
#!/bin/bash
apt-get update
apt-get install -y nginx
EOT
<<-EOT(带短横)会自动剥掉每行共同的缩进,写脚本/cloud-init 很方便。
8. 一个能跑的小例子
# main.tf
terraform {
required_version = ">= 1.5.0, < 1.10.0"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.40" }
}
}
provider "aws" {
region = var.region
}
variable "region" {
type = string
default = "ap-guangzhou"
}
variable "azs" {
type = list(string)
default = ["ap-guangzhou-3", "ap-guangzhou-4"]
}
locals {
name_prefix = "demo"
}
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg"
dynamic "ingress" {
for_each = [22, 80, 443]
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
output "sg_id" {
value = aws_security_group.web.id
}
terraform validate 应直接通过(不需要凭证)。
9. 练习与验收
练习 A
- 把上面第 8 节的例子存成
main.tf,跑terraform fmt与terraform validate都通过 - 给
region加一个validation,拒绝包含大写字母的值
练习 B
- 用
for_each把「3 台 web」改成按 map 生成(key 为web-1/web-2/web-3) - 试着删除其中一台,观察
terraform plan里是否只有那一台被影响(体会 for_each 的稳定性)
验收清单
- 能区分
resource的普通参数与嵌套块 - 会用
count与for_each,并理解二者对「增删项」的不同影响 - 会用
variable的type/default/validation/sensitive - 能用
locals与merge/cidrsubnet等函数拼出标签与网段 - 能写出通过
terraform validate的配置
下一章:Provider 与多环境 —— 配多个云账号/地域,并用 workspace 与目录分流管理 staging / prod。