表达式与函数

掌握 Terraform 的表达式和内置函数,让配置更灵活强大。

一、表达式基础

1.1 字符串插值

locals {
  project = "myapp"
  env     = "prod"
  
  # 基础插值
  name = "${local.project}-${local.env}"
  
  # 表达式插值
  instance_name = "${local.project}-${local.env}-${count.index + 1}"
  
  # 条件插值
  url = "https://${local.env == "prod" ? "www" : local.env}.example.com"
}

1.2 条件表达式

locals {
  # 三元运算符
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
  
  # 嵌套条件
  db_size = (
    var.environment == "prod" ? "db.r5.2xlarge" :
    var.environment == "staging" ? "db.t3.large" :
    "db.t3.micro"
  )
  
  # 布尔条件
  create_lb = var.instance_count > 1 ? true : false
}

1.3 for 表达式

列表推导:

locals {
  # 转换列表
  uppercase_names = [for s in var.names : upper(s)]
  
  # 过滤列表
  prod_instances = [
    for inst in var.instances :
    inst if inst.environment == "prod"
  ]
  
  # 带索引
  indexed_items = [
    for i, v in var.items :
    "${i}: ${v}"
  ]
}

映射推导:

locals {
  # 创建映射
  instance_ips = {
    for instance in aws_instance.app :
    instance.id => instance.private_ip
  }
  
  # 分组
  instances_by_az = {
    for az in var.availability_zones :
    az => [
      for inst in aws_instance.app :
      inst.id if inst.availability_zone == az
    ]
  }
  
  # 转换映射
  env_tags = {
    for k, v in var.tags :
    upper(k) => "${v}-${var.environment}"
  }
}

1.4 splat 表达式

# 获取所有实例的 ID
instance_ids = aws_instance.app[*].id

# 等价于
instance_ids = [for inst in aws_instance.app : inst.id]

# 嵌套属性
subnet_cidrs = aws_subnet.public[*].cidr_block

# 条件 splat
available_instances = aws_instance.app[*].id if var.create_instances

二、字符串函数

2.1 格式化函数

locals {
  # format - 格式化字符串
  name = format("%s-%s-%03d", var.project, var.env, count.index)
  # 输出: myapp-prod-001
  
  # join - 连接列表
  tags_string = join(",", var.tags)
  # 输出: "tag1,tag2,tag3"
  
  # split - 分割字符串
  cidr_parts = split("/", "10.0.0.0/16")
  # 输出: ["10.0.0.0", "16"]
  
  # trimspace - 移除首尾空格
  clean_name = trimspace("  myapp  ")
  # 输出: "myapp"
}

2.2 大小写函数

locals {
  # upper / lower
  upper_env = upper(var.environment)  # "PROD"
  lower_env = lower(var.environment)  # "prod"
  
  # title - 首字母大写
  title_name = title("hello world")   # "Hello World"
  
  # replace - 替换
  sanitized = replace(var.bucket_name, "_", "-")
}

2.3 子串函数

locals {
  # substr(string, offset, length)
  short_id = substr(aws_instance.app.id, 0, 8)
  
  # strrev - 反转字符串
  reversed = strrev("terraform")  # "mrofrarret"
  
  # trimprefix / trimsuffix
  clean_url = trimprefix("https://example.com", "https://")
  # 输出: "example.com"
}

三、集合函数

3.1 列表函数

locals {
  zones = ["us-west-2a", "us-west-2b", "us-west-2c"]
  
  # length - 长度
  zone_count = length(local.zones)  # 3
  
  # element - 获取元素(循环)
  primary_zone = element(local.zones, 0)
  wrapped_zone = element(local.zones, 5)  # 循环,返回 zones[2]
  
  # index - 查找索引
  zone_index = index(local.zones, "us-west-2b")  # 1
  
  # contains - 检查包含
  has_zone = contains(local.zones, "us-west-2a")  # true
}

3.2 列表操作

locals {
  list1 = [1, 2, 3]
  list2 = [3, 4, 5]
  
  # concat - 连接列表
  combined = concat(local.list1, local.list2)
  # 输出: [1, 2, 3, 3, 4, 5]
  
  # distinct - 去重
  unique_items = distinct(local.combined)
  # 输出: [1, 2, 3, 4, 5]
  
  # flatten - 展平嵌套列表
  nested = [[1, 2], [3, 4]]
  flat = flatten(local.nested)
  # 输出: [1, 2, 3, 4]
  
  # slice - 切片
  subset = slice(local.list1, 0, 2)
  # 输出: [1, 2]
  
  # reverse - 反转
  reversed = reverse(local.list1)
  # 输出: [3, 2, 1]
}

3.3 集合操作

locals {
  set1 = toset([1, 2, 3])
  set2 = toset([2, 3, 4])
  
  # setunion - 并集
  union = setunion(local.set1, local.set2)
  # 输出: [1, 2, 3, 4]
  
  # setintersection - 交集
  intersection = setintersection(local.set1, local.set2)
  # 输出: [2, 3]
  
  # setsubtract - 差集
  difference = setsubtract(local.set1, local.set2)
  # 输出: [1]
}

3.4 映射函数

locals {
  tags = {
    Name        = "MyApp"
    Environment = "Prod"
  }
  
  # keys - 获取键列表
  tag_keys = keys(local.tags)
  # 输出: ["Name", "Environment"]
  
  # values - 获取值列表
  tag_values = values(local.tags)
  # 输出: ["MyApp", "Prod"]
  
  # lookup - 查找值
  env = lookup(local.tags, "Environment", "default")
  
  # merge - 合并映射
  all_tags = merge(
    local.tags,
    { Owner = "Platform" }
  )
  
  # zipmap - 创建映射
  zone_map = zipmap(
    ["a", "b", "c"],
    ["us-west-2a", "us-west-2b", "us-west-2c"]
  )
  # 输出: {a = "us-west-2a", b = "us-west-2b", c = "us-west-2c"}
}

四、数值和类型函数

4.1 数值函数

locals {
  # min / max
  minimum = min(5, 12, 9)        # 5
  maximum = max(5, 12, 9)        # 12
  
  # ceil / floor
  rounded_up = ceil(4.2)         # 5
  rounded_down = floor(4.8)      # 4
  
  # abs - 绝对值
  absolute = abs(-42)            # 42
  
  # pow - 幂运算
  squared = pow(2, 3)            # 8
  
  # log - 对数
  logarithm = log(8, 2)          # 3
}

4.2 类型转换

locals {
  # tobool
  is_enabled = tobool("true")
  
  # tonumber
  port = tonumber("8080")
  
  # tostring
  port_string = tostring(8080)
  
  # tolist
  zones_list = tolist(["a", "b"])
  
  # toset
  zones_set = toset(["a", "b", "a"])  # 去重
  
  # tomap
  config_map = tomap({
    key = "value"
  })
}

4.3 类型检查

locals {
  # can - 测试表达式是否有效
  is_valid_cidr = can(cidrhost(var.vpc_cidr, 0))
  
  # try - 尝试多个表达式
  instance_ip = try(
    aws_instance.app.public_ip,
    aws_instance.app.private_ip,
    "0.0.0.0"
  )
  
  # type - 获取类型
  value_type = type(var.instance_count)  # "number"
}

五、编码和哈希函数

5.1 编码函数

locals {
  # base64encode / base64decode
  encoded = base64encode("hello")
  decoded = base64decode(local.encoded)
  
  # jsonencode / jsondecode
  json_string = jsonencode({
    name = "myapp"
    port = 8080
  })
  
  config = jsondecode(file("config.json"))
  
  # yamlencode / yamldecode
  yaml_string = yamlencode({
    name = "myapp"
    port = 8080
  })
}

5.2 哈希函数

locals {
  # md5
  file_hash = md5(file("script.sh"))
  
  # sha1 / sha256 / sha512
  password_hash = sha256(var.password)
  
  # base64sha256 / base64sha512
  content_hash = base64sha256(filebase64("archive.zip"))
  
  # uuid / uuidv5
  random_id = uuid()
  namespace_id = uuidv5("dns", "example.com")
}

六、文件系统函数

6.1 文件操作

locals {
  # file - 读取文件内容
  script_content = file("${path.module}/scripts/init.sh")
  
  # filebase64 - Base64 编码读取
  binary_content = filebase64("image.png")
  
  # templatefile - 模板渲染
  user_data = templatefile("${path.module}/user-data.tpl", {
    hostname = "web-server"
    region   = var.aws_region
  })
  
  # fileexists - 检查文件是否存在
  has_config = fileexists("${path.module}/config.json")
}

6.2 路径函数

locals {
  # path.module - 当前模块路径
  module_path = path.module
  
  # path.root - 根模块路径
  root_path = path.root
  
  # path.cwd - 当前工作目录
  current_dir = path.cwd
  
  # basename - 文件名
  filename = basename("/path/to/file.txt")  # "file.txt"
  
  # dirname - 目录名
  directory = dirname("/path/to/file.txt")  # "/path/to"
  
  # abspath - 绝对路径
  absolute = abspath("./config")
}

七、网络函数

7.1 CIDR 函数

locals {
  vpc_cidr = "10.0.0.0/16"
  
  # cidrhost - 计算主机地址
  first_ip = cidrhost(local.vpc_cidr, 0)      # "10.0.0.0"
  gateway  = cidrhost(local.vpc_cidr, 1)      # "10.0.0.1"
  
  # cidrnetmask - 获取子网掩码
  netmask = cidrnetmask(local.vpc_cidr)       # "255.255.0.0"
  
  # cidrsubnet - 创建子网
  public_subnet_1 = cidrsubnet(local.vpc_cidr, 8, 0)   # "10.0.0.0/24"
  public_subnet_2 = cidrsubnet(local.vpc_cidr, 8, 1)   # "10.0.1.0/24"
  private_subnet_1 = cidrsubnet(local.vpc_cidr, 8, 10) # "10.0.10.0/24"
  
  # cidrsubnets - 批量创建子网
  subnets = cidrsubnets(local.vpc_cidr, 8, 8, 8, 8)
  # 输出: ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

7.2 实际应用

# 自动划分子网
locals {
  vpc_cidr = "10.0.0.0/16"
  az_count = length(var.availability_zones)
  
  # 为每个 AZ 创建公有和私有子网
  public_subnets = cidrsubnets(
    local.vpc_cidr,
    [for i in range(local.az_count) : 8]...
  )
  
  private_subnets = cidrsubnets(
    cidrsubnet(local.vpc_cidr, 1, 1),
    [for i in range(local.az_count) : 8]...
  )
}

resource "aws_subnet" "public" {
  count             = local.az_count
  vpc_id            = aws_vpc.main.id
  cidr_block        = local.public_subnets[count.index]
  availability_zone = var.availability_zones[count.index]
}

八、日期和时间函数

locals {
  # timestamp - 当前时间戳
  current_time = timestamp()
  # 输出: "2024-01-10T12:00:00Z"
  
  # formatdate - 格式化日期
  formatted_date = formatdate("YYYY-MM-DD", timestamp())
  # 输出: "2024-01-10"
  
  # timeadd - 时间加法
  future_time = timeadd(timestamp(), "24h")
  
  # timecmp - 时间比较
  is_after = timecmp(timestamp(), "2024-01-01T00:00:00Z")
  # 返回: 1 (当前时间在后), 0 (相等), -1 (在前)
}

九、实用函数组合

9.1 动态标签生成

locals {
  base_tags = {
    Environment = var.environment
    ManagedBy   = "Terraform"
    CreatedAt   = formatdate("YYYY-MM-DD", timestamp())
  }
  
  # 为不同资源添加特定标签
  instance_tags = merge(
    local.base_tags,
    {
      Name = "${var.project}-${var.environment}-instance"
      Type = "Compute"
    }
  )
  
  database_tags = merge(
    local.base_tags,
    {
      Name = "${var.project}-${var.environment}-db"
      Type = "Database"
    }
  )
}

9.2 配置验证

locals {
  # 验证 CIDR 格式
  is_valid_cidr = can(cidrhost(var.vpc_cidr, 0))
  
  # 验证端口范围
  is_valid_port = var.port >= 1 && var.port <= 65535
  
  # 验证环境名称
  is_valid_env = contains(["dev", "staging", "prod"], var.environment)
  
  # 组合验证
  validation_errors = concat(
    local.is_valid_cidr ? [] : ["Invalid CIDR block"],
    local.is_valid_port ? [] : ["Port out of range"],
    local.is_valid_env ? [] : ["Invalid environment"]
  )
}

# 在 variable 中使用
variable "vpc_cidr" {
  type = string
  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be a valid CIDR block."
  }
}

9.3 资源命名标准化

locals {
  # 标准化命名函数
  resource_name = lower(replace(
    "${var.project}-${var.environment}-${var.component}",
    "_",
    "-"
  ))
  
  # S3 桶名称(全局唯一)
  bucket_name = "${local.resource_name}-${md5(data.aws_caller_identity.current.account_id)}"
  
  # DNS 名称
  dns_name = "${local.resource_name}.${var.domain}"
}

十、性能和最佳实践

10.1 避免重复计算

# ❌ 避免
resource "aws_instance" "app" {
  count = 3
  tags = {
    Name = "${var.project}-${upper(var.environment)}-${count.index}"
  }
}

# ✅ 推荐
locals {
  name_prefix = "${var.project}-${upper(var.environment)}"
}

resource "aws_instance" "app" {
  count = 3
  tags = {
    Name = "${local.name_prefix}-${count.index}"
  }
}

10.2 简化复杂表达式

# ❌ 难以理解
instance_type = var.env == "prod" ? (var.size == "large" ? "t3.2xlarge" : "t3.large") : (var.size == "large" ? "t3.large" : "t3.micro")

# ✅ 使用 locals 分解
locals {
  size_map = {
    prod = {
      large  = "t3.2xlarge"
      medium = "t3.large"
    }
    dev = {
      large  = "t3.large"
      medium = "t3.micro"
    }
  }
  
  instance_type = local.size_map[var.env][var.size]
}

10.3 错误处理

locals {
  # 使用 try 提供回退值
  config = try(
    jsondecode(file("config.json")),
    {
      default = "value"
    }
  )
  
  # 使用 can 检查有效性
  use_custom_ami = can(data.aws_ami.custom[0].id) ? data.aws_ami.custom[0].id : data.aws_ami.default.id
}

小结

Terraform 的表达式和函数体系:

  • 表达式: for、条件、splat
  • 字符串: format、join、split
  • 集合: concat、merge、lookup
  • 类型: can、try、type
  • 编码: json、yaml、base64
  • 网络: cidr 系列函数
  • 文件: file、templatefile

掌握这些工具,可以写出更简洁、更强大的 Terraform 配置。