跳到主要内容

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 vs for_each
  • 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 章)
}
字段作用
typestring / 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 不是变量

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

  1. 把上面第 8 节的例子存成 main.tf,跑 terraform fmt 与 terraform validate 都通过
  2. 给 region 加一个 validation,拒绝包含大写字母的值

练习 B

  1. 用 for_each 把「3 台 web」改成按 map 生成(key 为 web-1/web-2/web-3)
  2. 试着删除其中一台,观察 terraform plan 里是否只有那一台被影响(体会 for_each 的稳定性)

验收清单

  • 能区分 resource 的普通参数与嵌套块
  • 会用 count 与 for_each,并理解二者对「增删项」的不同影响
  • 会用 variable 的 type / default / validation / sensitive
  • 能用 locals 与 merge / cidrsubnet 等函数拼出标签与网段
  • 能写出通过 terraform validate 的配置

下一章:Provider 与多环境 —— 配多个云账号/地域,并用 workspace 与目录分流管理 staging / prod。