3、HCL语法:变量、资源、数据源、输出、模块的基本语法

Terraform 的核心价值在于将云基础设施以声明式代码的形式进行管理,而这一切的基石便是其专用的配置语言——HCL(HashiCorp Configuration Language)。HCL 设计得既对人类友好,又便于机器解析,它不仅是 Terraform 的配置语言,也被用于 Consul、Vault 等 HashiCorp 生态工具。本章将深入剖析 HCL 中最核心的五个语法块:变量(Variable)、资源(Resource)、数据源(Data Source)、输出(Output)和模块(Module)。理解这些基础语法是编写可维护、可复用、生产级 Terraform 代码的必经之路。我们将从每个语法块的核心原理出发,结合实战案例与常见误区,帮助你建立扎实的 HCL 语法功底。

HCL 核心语法块关系与数据流 变量 (Variable) 输入参数 类型约束/默认值 敏感标记 数据源 (Data Source) 只读查询 获取外部资源属性 引用现有基础设施 资源 (Resource) 核心管理对象 创建/更新/删除 依赖关系 模块 (Module) 封装与复用 跨目录调用 版本管理 输出 (Output) 对外暴露属性 命令行展示 模块间传递 变量提供输入 → 资源/数据源消费 → 输出暴露结果 → 模块封装复用 虚线表示引用关系,实线表示数据流向

上图清晰地展示了 HCL 中五个核心语法块之间的协作关系。变量作为输入参数,为资源和数据源提供配置值;数据源用于查询外部已有的基础设施信息,供资源引用;资源是 Terraform 管理的核心对象,负责创建、更新和删除云资源;输出则用于对外暴露资源或数据源的属性,方便用户查看或供其他模块调用;模块则是将这些语法块进行封装和复用的高级抽象。接下来,我们将逐一深入解析每个语法块。

3.1 变量(Variable):参数化你的配置

核心原理

变量是 HCL 中实现配置参数化的关键机制。它允许你将硬编码的值提取出来,使得同一份 Terraform 配置可以在不同环境(如开发、测试、生产)中复用,而无需修改代码逻辑。变量在声明时可以不指定值,而在运行 terraform plan 或 terraform apply 时通过命令行、环境变量或 .tfvars 文件传入。

判断方法与操作流程

声明一个变量使用 variable 块,其基本语法结构如下:

variable "instance_type" {
  description = "The EC2 instance type"
  type        = string
  default     = "t3.micro"
  sensitive   = true
}
  • description:可选,用于描述变量的用途,增强代码可读性。
  • type:可选,指定变量的类型,如 string、number、bool、list、map、object 等。如果不指定,默认为 any。
  • default:可选,为变量设置默认值。如果未设置默认值,且未在运行时传入,Terraform 会交互式提示用户输入。
  • sensitive:可选,设置为 true 后,该变量在 CLI 输出和日志中会被隐藏,适用于密码、密钥等敏感信息。

变量的赋值优先级从高到低为:命令行 -var 参数 > 环境变量 TF_VAR_<变量名> > terraform.tfvars 文件 > terraform.tfvars.json 文件 > *.auto.tfvars 文件 > 变量定义中的 default 值。

案例拆解:多环境部署配置

假设我们需要为开发和生产环境部署不同的 EC2 实例规格。我们可以定义变量 instance_type 和 ami_id,然后创建两个 .tfvars 文件:

# dev.tfvars
instance_type = "t3.micro"
ami_id        = "ami-0c55b159cbfafe1f0"

# prod.tfvars
instance_type = "m5.large"
ami_id        = "ami-0c55b159cbfafe1f0"

在运行 Terraform 时,通过 -var-file 参数指定对应的文件:

terraform apply -var-file="dev.tfvars"

这种方式使得环境切换变得极其简单,且避免了在代码中硬编码环境差异。

常见误区:不要将敏感信息(如数据库密码)直接写在 default 值或 .tfvars 文件中并提交到版本控制。应使用环境变量或 Terraform Cloud 的变量管理功能,并配合 sensitive = true 标记。

3.2 资源(Resource):基础设施的核心单元

核心原理

资源是 Terraform 中最核心的语法块,它代表一个具体的云基础设施对象,如一台虚拟机、一个 VPC、一个 DNS 记录等。Terraform 通过资源块来声明期望的最终状态,并负责将当前状态收敛到期望状态。每个资源块都包含资源类型和资源名称,两者组合起来在配置中必须是唯一的。

操作流程与语法

资源的基本语法如下:

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"

  tags = {
    Name = "Web Server"
  }
}
  • 资源类型(如 aws_instance):由 Provider 提供,决定了资源的种类。
  • 资源名称(如 web_server):在同一个 Terraform 配置中,用于唯一标识该资源。在配置内部,可以通过 aws_instance.web_server 来引用该资源。
  • 参数:资源块内部的参数是特定于资源类型的,用于定义资源的属性。这些参数可以是字符串、数字、列表、映射,甚至可以是另一个资源的属性引用。

资源之间可以通过 隐式依赖 和 显式依赖 来构建关系。隐式依赖是指通过引用其他资源的属性自动建立的依赖;显式依赖则通过 depends_on 参数手动指定。

案例拆解:Web 服务器与安全组

我们创建一个安全组和一个 EC2 实例,并让实例引用安全组的 ID:

resource "aws_security_group" "web_sg" {
  name_prefix = "web-sg-"
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "web_server" {
  ami                    = "ami-0c55b159cbfafe1f0"
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.web_sg.id]

  tags = {
    Name = "Web Server"
  }
}

这里,aws_instance.web_server 引用了 aws_security_group.web_sg.id,Terraform 会自动识别出这种隐式依赖,并在创建实例之前先创建安全组。

经验提醒:尽量使用隐式依赖,它能让 Terraform 自动优化资源创建和销毁的顺序。只有在无法通过引用表达依赖关系时(例如,需要确保一个资源在另一个资源之后被销毁),才使用 depends_on。

3.3 数据源(Data Source):读取外部基础设施信息

核心原理

数据源允许 Terraform 读取在配置文件之外定义的信息,或者由其他 Terraform 配置管理的信息。它执行的是只读操作,不会创建、修改或删除任何资源。数据源非常适合用于获取已有的 AMI ID、VPC ID、子网 ID,或者查询当前 AWS 账号下的可用区列表等。

操作流程与语法

数据源的基本语法与资源类似,但使用 data 关键字:

data "aws_ami" "ubuntu" {
  most_recent = true
  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"]
  }
  owners = ["099720109477"] # Canonical
}

引用数据源的属性时,使用 data.<数据源类型>.<名称>.<属性> 的格式。例如,要获取上述 AMI 的 ID,可以使用 data.aws_ami.ubuntu.id。

案例拆解:动态获取最新 AMI

在创建 EC2 实例时,我们通常不希望硬编码 AMI ID,因为 AMI 会不断更新。使用数据源可以动态获取最新的 AMI:

data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

resource "aws_instance" "web_server" {
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = "t3.micro"
}

这样,每次执行 terraform apply 时,Terraform 都会查询最新的 Amazon Linux 2 AMI,确保使用的是最新的系统镜像。

常见误区:数据源的结果在 terraform plan 阶段就会被获取并缓存。如果数据源查询的结果在 plan 和 apply 之间发生了变化(例如,AMI 被注销),可能会导致 apply 失败。对于这种场景,需要重新运行 terraform plan。

3.4 输出(Output):暴露配置结果

核心原理

输出块用于定义 Terraform 配置执行完成后,需要向用户展示或提供给其他配置使用的值。它类似于编程语言中的返回值。输出值可以是资源属性、数据源属性,也可以是变量或任何有效的 HCL 表达式。

操作流程与语法

输出块的基本语法如下:

output "instance_ip" {
  value       = aws_instance.web_server.public_ip
  description = "The public IP of the web server"
  sensitive   = false
}
  • value:必填,指定要输出的值。
  • description:可选,描述输出的用途。
  • sensitive:可选,如果设置为 true,该输出值在 CLI 中会被隐藏,但可以通过 terraform output 命令查看。

执行 terraform apply 后,输出值会显示在 CLI 的末尾。你也可以随时使用 terraform output 命令来查询输出值。

案例拆解:模块间传递信息

输出在模块化设计中扮演着至关重要的角色。当根模块调用子模块时,子模块可以通过输出将内部资源的关键信息(如 ID、IP 地址)暴露给根模块,根模块再将这些信息用于其他资源的配置或作为最终的输出。

# 在子模块中定义输出
output "vpc_id" {
  value = aws_vpc.main.id
}

# 在根模块中调用子模块并引用其输出
module "network" {
  source = "./modules/network"
}

resource "aws_instance" "web_server" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.micro"
  subnet_id     = module.network.vpc_id
}
经验提醒:对于包含敏感信息的输出(如数据库密码),务必设置 sensitive = true,防止在日志或 CI/CD 系统中泄露。

3.5 模块(Module):封装与复用

核心原理

模块是 Terraform 中实现代码复用和抽象的核心机制。一个模块就是一组放在同一目录下的 .tf 文件,它们共同定义了一组相关的资源。模块可以像资源一样被调用,并且可以接收输入变量(通过 variable 定义)并返回输出值(通过 output 定义)。模块可以来自本地目录、Terraform Registry、GitHub 等。

操作流程与语法

调用模块使用 module 块:

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24"]
}
  • source:必填,指定模块的来源。可以是本地路径(如 ./modules/vpc),也可以是远程地址(如 github.com/.../vpc)。
  • version:可选,指定模块的版本,仅用于从 Registry 获取的模块。
  • 其他参数:传递给模块的输入变量,需要与模块内部定义的 variable 块匹配。

案例拆解:构建可复用的网络模块

我们创建一个简单的 VPC 模块,包含 VPC、子网和互联网网关:

# modules/network/main.tf
variable "vpc_cidr" {
  type = string
}

variable "subnet_cidr" {
  type = string
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr
}

resource "aws_subnet" "main" {
  vpc_id     = aws_vpc.main.id
  cidr_block = var.subnet_cidr
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "subnet_id" {
  value = aws_subnet.main.id
}

在根模块中调用这个模块:

module "network" {
  source = "./modules/network"

  vpc_cidr    = "10.0.0.0/16"
  subnet_cidr = "10.0.1.0/24"
}

output "vpc_id" {
  value = module.network.vpc_id
}

通过这种方式,我们可以将复杂的网络架构封装成一个可复用的模块,并在多个项目中共享。

常见误区:模块的 source 路径如果使用本地相对路径,必须确保路径正确。在团队协作中,推荐将模块发布到 Terraform Registry 或 Git 仓库,并指定版本号,以保证环境的一致性和可追溯性。

3.6 总结与复盘

语法块 核心作用 关键关键字 典型使用场景
变量 参数化配置,提高复用性 variable, type, default, sensitive 环境区分、用户输入、敏感信息管理
资源 声明和管理基础设施对象 resource, depends_on 创建 EC2、VPC、RDS 等所有云资源
数据源 只读查询外部信息 data, filter 获取最新 AMI、查询现有 VPC、获取可用区
输出 暴露配置结果,供外部使用 output, value, sensitive 展示 IP 地址、传递模块间数据
模块 封装和复用配置代码 module, source, version 抽象网络、计算、存储等通用架构

在本章中,我们系统地学习了 HCL 的五大核心语法块。变量让配置变得灵活,资源是管理的核心,数据源打通了与外部世界的连接,输出提供了结果的可见性,而模块则赋予了代码复用的能力。掌握这些基础语法后,你就能读懂绝大多数 Terraform 配置,并开始编写自己的基础设施代码。在实际项目中,请务必注意敏感信息的处理、依赖关系的正确表达以及模块的版本管理,这些是构建生产级 Terraform 代码的关键要素。