Terraform 核心语法

Terraform 的配置语言叫 HCL(HashiCorp Configuration Language)。它的设计目标是:人类可读、可编写、易于自动化。相比 JSON,HCL 更简洁;相比 YAML,HCL 支持更丰富的语法特性。

这一篇,我们深入讲解 Terraform 的核心语法。

基础结构

单文件示例

main.tf
# 声明 Provider 版本约束
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-east-1"
  }
}

# Provider 配置
provider "aws" {
  region = "us-east-1"
}

# 资源定义
resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t3.medium"

  tags = {
    Name        = "web-server"
    Environment = "prod"
  }
}

变量

变量定义

variables.tf
variable "instance_type" {
  description = "EC2 实例类型"
  type        = string
  default     = "t3.micro"

  validation {
    condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
    error_message = "实例类型必须是 t3.micro、t3.small 或 t3.medium。"
  }
}

variable "environment" {
  description = "环境名称"
  type        = string
}

variable "ami_id" {
  description = "AMI ID"
  type        = string
}

variable "tags" {
  description = "额外标签"
  type        = map(string)
  default     = {}
}

变量类型

types.tf
# 字符串
variable "string_var" {
  type    = string
  default = "hello"
}

# 数字
variable "count" {
  type    = number
  default = 3
}

# 布尔值
variable "enable_monitoring" {
  type    = bool
  default = true
}

# 列表
variable "azs" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

# 对象
variable "db_config" {
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    storage_gb     = number
  })
  default = {
    engine         = "mysql"
    engine_version = "8.0"
    instance_class = "db.t3.micro"
    storage_gb     = 20
  }
}

# Map
variable "instance_types" {
  type = map(string)
  default = {
    dev    = "t3.micro"
    staging = "t3.small"
    prod   = "t3.medium"
  }
}

使用变量

main.tf
resource "aws_instance" "web" {
  count         = var.count
  ami           = var.ami_id
  instance_type = var.instance_types[var.environment]

  tags = merge(
    var.tags,
    { Environment = var.environment }
  )
}

资源

资源语法

resource
resource "<resource_type>" "<local_name>" {
  # 资源配置
}

资源引用

resources.tf
# 引用本文件或其他文件中的资源
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

# 引用资源的属性
resource "aws_subnet" "public" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}

# 引用被依赖资源的属性
output "vpc_id" {
  value = aws_vpc.main.id
}

count 和 for_each

使用
resource "aws_instance" "web" {
  count = 3

  ami           = "ami-12345678"
  instance_type = "t3.micro"
  subnet_id     = aws_subnet.public[count.index].id

  tags = {
    Name = "web-${count.index}"
  }
}

# 引用
aws_instance.web[0].id
aws_instance.web[*].id  # 全部
使用
resource "aws_security_group_rule" "http" {
  for_each = toset(["80", "443"])

  type              = "ingress"
  from_port         = each.value
  to_port           = each.value
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.web.id

  description = "Port ${each.value}"
}

# 引用
keys(aws_security_group_rule.http)
values(aws_security_group_rule.http)
count vs for_each
  • count:适用于创建相同类型的多个资源,通过索引引用
  • for_each:适用于创建需要唯一标识的资源,通过键引用
  • 建议:大多数情况下使用 for_each,因为删除中间元素时不会导致索引混乱 :::

数据源

数据源(Data Sources)允许读取只读信息,不创建资源。

data_sources.tf
# 获取 AWS Account ID
data "aws_caller_identity" "current" {}

output "account_id" {
  value = data.aws_caller_identity.current.account_id
}

# 获取 AMI 信息
data "aws_ami" "ubuntu" {
  most_recent = true
  owners      = ["099720109477"]  # Canonical

  filter {
    name   = "name"
    values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

# 获取可用区
data "aws_availability_zones" "available" {
  state = "available"
}

# 获取 IAM 策略文档
data "aws_iam_policy_document" "ecs_task" {
  statement {
    sid = "ECSContainerTaskExecution"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }

    actions = [
      "ecr:GetAuthorizationToken",
      "ecr:BatchCheckLayerAvailability",
      "ecr:GetDownloadUrlForLayer",
      "ecs:StartTask"
    ]

    resources = ["*"]
  }
}

本地值

本地值(Locals)在模块内定义可重用的表达式。

locals.tf
locals {
  # 拼接字符串
  project_name = "myapp"

  # 引用变量
  environment = var.environment

  # 组合标签
  common_tags = {
    Project     = local.project_name
    Environment = local.environment
    ManagedBy   = "Terraform"
    Owner       = "platform-team"
  }

  # 计算值
  all_azs = concat(data.aws_availability_zones.available.names, ["us-east-1d"])

  # 条件表达式
  instance_type = local.environment == "prod" ? "t3.medium" : "t3.micro"
}

# 使用
resource "aws_instance" "web" {
  tags = local.common_tags
}

输出

输出值(Outputs)用于公开资源的属性或计算结果。

outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "instance_ips" {
  description = "所有实例的公有 IP"
  value       = aws_instance.web[*].public_ip
}

output "security_group_rules" {
  description = "安全组规则"
  value       = aws_security_group_rule.http

  # 敏感输出
  sensitive = true
}

output "db_endpoint" {
  description = "数据库连接地址"
  value       = "${aws_db_instance.main.endpoint}:${aws_db_instance.main.port}"
}

条件表达式

conditionals.tf
# 三元表达式
locals {
  instance_type = var.environment == "prod" ? "t3.medium" : "t3.micro"
}

# count 控制资源创建
resource "aws_instance" "web" {
  count = var.create_production ? 5 : 1

  ami           = "ami-12345678"
  instance_type = "t3.micro"
}

# for_each 条件
resource "aws_security_group_rule" "http" {
  for_each = var.enable_https ? toset(["80", "443"]) : toset(["80"])

  # ...
}

循环

表达式循环

for
# 列表推导
locals {
  envs = ["dev", "staging", "prod"]
  prefixed = [for env in local.envs : "${env}-server"]
  # 结果: ["dev-server", "staging-server", "prod-server"]
}

# Map 推导
locals {
  config = {
    for k, v in var.instance_config : k => upper(v)
  }
  # 如果 instance_config = {a = "foo", b = "bar"}
  # 结果: {a = "FOO", b = "BAR"}
}

# 带过滤的推导
locals {
  filtered = [
    for name, instance in aws_instance.all :
    name if instance.ami != "ami-old"
  ]
}

动态块

dynamic_blocks.tf
resource "aws_security_group" "web" {
  name        = "web-sg"
  description = "Web 服务器安全组"

  # 动态 ingress 规则
  dynamic "ingress" {
    for_each = var.ingress_rules
    iterator = rule

    content {
      description = rule.value.description
      from_port   = rule.value.port
      to_port     = rule.value.port
      protocol    = rule.value.protocol
      cidr_blocks = rule.value.cidr_blocks
    }
  }
}
使用
variable "ingress_rules" {
  type = list(object({
    description = string
    port        = number
    protocol    = string
    cidr_blocks = list(string)
  }))

  default = [
    {
      description = "HTTP"
      port        = 80
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    },
    {
      description = "HTTPS"
      port        = 443
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
    }
  ]
}

函数

Terraform 内置了大量函数:

functions.tf
locals {
  # 字符串函数
  name  = lower("WEB-SERVER")           # "web-server"
  parts = split("-", "web-server")       # ["web", "server"]
  joined = join("/", ["path", "to", "file"])  # "path/to/file"

  # 数值函数
  max_val = max(1, 5, 3)                 # 5
  min_val = min(1, 5, 3)                # 1

  # Map 函数
  merged_tags = merge({A = 1}, {B = 2}, {A = 10})  # {A = 10, B = 2}
  keys_list  = keys({a = 1, b = 2})     # ["a", "b"]

  # 文件函数
  file_content = file("${path.module}/config.json")

  # 时间函数
  timestamp_now = timestamp()           # 当前时间戳
  formatted     = formatdate("YYYY-MM-DD", timestamp())  # "2024-01-15"

  # Base64 编码
  encoded = base64encode("hello")       # "aGVsbG8="

  # 正则表达式
  matched = regex("^([a-z]+)-([a-z]+)$", "web-server")
  # 结果: ["web", "server"]
}

模块调用

modules/web_cluster/main.tf
variable "cluster_size" {
  description = "集群中实例数量"
  type        = number
}

variable "instance_type" {
  description = "实例类型"
  type        = string
}

variable "environment" {
  description = "环境名称"
  type        = string
}

resource "aws_instance" "cluster" {
  count = var.cluster_size

  ami           = var.ami_id
  instance_type = var.instance_type

  tags = {
    Name = "cluster-${var.environment}-${count.index}"
  }
}

output "instance_ids" {
  value = aws_instance.cluster[*].id
}
调用模块
module "web_cluster" {
  source = "./modules/web_cluster"

  cluster_size  = 3
  instance_type = "t3.medium"
  environment   = "prod"
  ami_id        = data.aws_ami.ubuntu.id
}

# 引用模块输出
module.web_cluster.instance_ids

导入现有资源

# 生成 import 指令
terraform import aws_instance.web i-1234567890abcdef0
导入目标配置
resource "aws_instance" "web" {
  # 必须有资源配置,否则 import 后会显示 "no configuration"
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  # Terraform 会填充其他属性
}
批量导入
# 创建一个 import.tf 文件
import {
  id = "i-1234567890abcdef0"
  to = aws_instance.web
}

import {
  id = "sg-12345678"
  to = aws_security_group.web
}

常见错误

引用不存在的属性

错误
resource "aws_instance" "web" {
  ami = aws_ami.ubuntu.id
}

如果 aws_ami.ubuntu 不存在或类型不对,会报错。

循环依赖

错误
resource "aws_instance" "a" {
  depends_on = [aws_instance.b]
}

resource "aws_instance" "b" {
  depends_on = [aws_instance.a]
}

Terraform 会报错:Cycle: aws_instance.a, aws_instance.b

类型不匹配

错误
variable "list" {
  type = list(string)
}

# 使用时传入 number
instance_type = var.list[0]  # 如果 list = [1, 2, 3] 就会报错

:::info 下一步

Terraform 状态是核心概念,想深入了解?请阅读 Terraform State 管理