跳至内容
Terraform核心对象

Terraform核心对象

核心对象

resource 资源 data 数据源 locals 局部变量 output 输出和模块导出 variable 变量 ephemeral 临时变量 moved 移动资源, 即重命名 removed 只删除本地状态内的对象, 不影响云上资源

resource 资源

资源定义

"资源模块名 + 状态名称" 需要全局唯一

resource "资源模块名" "状态名称" {}

元参数

每种资源都可用

resource "xxxxx" "xxxxx" {
    # 显式声明依赖关系
    depends_on: []
    # 创建多个资源实例
    count:

    # 迭代集合, 为集合中每一个元素创建一个对应的资源实例
    for_each:

    # 指定非默认 Provider 实例
    provider:

    # 自定义资源的生命周期行为
    lifecycle:

    # 默认是在资源 创建时执行 执行一些额外的操作
    provisioner:

    在资源创建后执行一些额外的操作
    connection:
}

如果创建的资源实例彼此之间几乎完全一致,那么 count 比较合适 如果多个资源参数差异较大, 那么使用 for_each 会更加安全合适

count

count 元参数接受一个整数, 并创建相应数量的资源或模块实例

  1. 一个资源或 Module 块不能同时使用 count 和 for_each
  2. 标号从 0 开始

示例: 创建4个主机


variable "host_names" {
  description = "List of host name prefixes"
  type        = list(string)
  default     = ["web", "app", "db"]
}

resource "aws_instance" "server" {
  count = length(var.host_names)        # 3
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # sever-web-1
  # sever-app-2
  # sever-db-3
  instance_name   = "server-${var.host_names[count.index]}-${count.index + 1}"

  tags = {
    Name = var.host_names[count.index]  # 读取标号
  }
}

output "my_server_1" {
  value = {
    data = aws_instance.server[0]   # 只引用了单个实例
  }
}

使用 count 条件式创建资源

variable "create_slb" {
  description = "Whether to create SLB"
  type        = bool
  default     = true
}

resource "alicloud_slb_load_balancer" "app" {
  # 只有当 create_slb = true 时创建资源; 即为 true 时, count 的值 = 0
  count = var.create_slb ? 1 : 0

  load_balancer_name = "app-lb"
  address_type       = "internet"
  load_balancer_spec = "slb.s2.small"

  tags = {
    Name = "ApplicationLB"
  }
}

for_each

针对 集合类型(set) 或 映射(map) 进行迭代创建资源

核心在于每个资源有一个固定的标识符 key, 这使得 Terraform 能够精确跟踪单个资源实例, 即使底层集合的顺序发生变化;

  1. 一个 resource 块不允许同时声明 count 与 for_each
  2. 不能直接使用列表(list), 需要转换为 set

each 对象 each.key — 对应于此实例的映射键(或集合成员); 必须是已知值, 不能是生成值; each.value — 对应于此实例的映射值; (如果提供的是集合, 则与 each.key 相同; )

resource "xxxxx" "rg" {
  # map 映射
  for_each = {
    a_group = "eastus"
    b_group = "westus2"
  }
  name     = each.key
  location = each.value
}

# list to set
resource "xxxx" "the-accounts" {
  for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
  name     = each.key
  display_name = "User ${each.key}"

}

示例2

variable "vpcs" {
  type = map(object({
    cidr_block = string
    zone_id    = string
  }))
}

resource "alicloud_vpc" "example" {
  for_each = var.vpcs
  vpc_name   = each.key
  cidr_block = each.value.cidr_block
}

resource "alicloud_vswitch" "example" {

  # 因为此时 alicloud_vpc.example 可以理解为就是一个 map 对象了;
  for_each = alicloud_vpc.example

  # each.value 是一个完整的 alicloud_vpc 对象
  vpc_id      = each.value.id

  # 此处理解为是从具体的 vpc 资源里面获取的 cidr_block 的值, 而不是从 vpcs 这个map里面获取的值
  cidr_block  = cidrsubnet(each.value.cidr_block, 8, 0)

  # each.key 是通过 alicloud_vpc.example 继承下来的; 反查 vpcs 里面的 value
  zone_id     = var.vpcs[each.key].zone_id
  vswitch_name = "${each.key}-default-vswitch"
}

output "vpc_ids" {
  value = {
    for k, v in alicloud_vpc.example : k => v.id
  }
}

在 for_each = alicloud_vpc.example 这一步时, 也一并继承了其关联的 vpcs 的键; 所以在 zone_id = var.vpcs[each.key].zone_id 这一步时能通过正确的key名进行查询;

示例3: 有时候需要 list to set

locals {
  vswitch_ids = toset(["vsw-abc123456", "vsw-def789012"])
}

lifecycle 生命周期

  lifecycle {

    # 忽略某些差异
    ignore_changes = [
      tags,
    ]

    # 运行前校验
    postcondition {
      condition     = self.tags["Component"] == "nomad-server"
      error_message = "tags[\"Component\"] must be \"nomad-server\"."
    }
  }

provisioner

默认是资源创建时执行

resource "aws_instance" "server" {
  provisioner "local-exec" {
    when    = destroy       # 设定为资源销毁时执行
    command = "echo 'Destroy-time provisioner'"
  }
}

变量

变量类型

基础类型:

  • string
  • number
  • bool

复合类型:

  • list() 本身是有序的
  • set() 一组不重复的值
  • map() 键类型必须是 string;
  • any: 任意类型
  • object((ATTR_NAME = ATTR_TYPE, ...))
  • tuple([,...])

map 只是kv结构, object 才是嵌入结构

变量引用: var.VAR_NAME

变量传入和优先级

  1. 默认值 variable 中的 default 值
  2. 文件 *.tfvars, *.tfvars.json, *.auto.tfvars, *.auto.tfvars.json
  3. 有 TF_VAR_ 的名称开头的系统环境变量会进行关联
  4. 命令行参数, -var 选项传递变量, -var-file 选项加载一个变量文件
  5. 当没有默认值, 且没有传入时, 会要求手动输入

注意: 其它非 TF_VAR_ 的名称开头变量是供应商模块运行所需自行读取的, 与用户自定义变量无关;

-var region=us-east-1
-var-file my-vars.tfvars
-var='image_id_list=["ami-abc123","ami-def456"]'
-var='image_id_map={"us-east-1":"ami-abc123","us-east-2":"ami-def456"}'

export TF_VAR_xxx=xxxxx

默认变量文件

.tfvars
.tfvars.json
terraform.tfvars
terraform.tfvars.json
.auto.tfvars
.auto.tfvars.json

variable 变量

variable "image_id" {
  type        = string          # 类型 string, number 等
  description = ""              # 描述
  default = "cn-beijing"        # 变量的默认值
  nullable = false              # 变量是否可为空, 禁止为 null
  sensitive = false             # 是否为秘密, 为 true 时输出时隐藏显示且不记录到日志

  # 定义变量验证规则, 为 true 时输入变量合法
  validation {

    # 验证条件
    # 验证 image_id 的值是否长度超过 4 并且是否以 ami- 为开头
    condition     = length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"

    # 验证失败后的消息输出
    error_message = "The image_id value must be a valid AMI id, starting with \"ami-\"."
  }
}

variable "env_list" {
  type        = list(string)
  description = "define environment name"
  default     = ["dev"]
}

variable "dns_record" {
  type        = map(string)
  description = "define dns name"
}

variable "ecs_info" {
  type = object({
    ecs_image = string,
    ecs_name  = string
  })
  description = "define ecs info"
}

ephemeral 临时变量

针对敏感秘密的场景, 不进行任何形式的持久化;

  1. 不在 console 和 log 文件中
  2. 不在 plan 计划文件中
  3. 不在 状态文件中
  4. 只能在特定上下文中引用临时变量

locals 局部变量

通过 locals 关键字进行定义, 通过 local.VAR_NAME 调用

局部值有助于避免在配置中多次重复相同的值或表达式, 多用于调试和测试

场景: 用一个比较复杂的表达式计算某一个值,并且反复使用, 这时把这个复杂表达式赋予一个局部值,然后多次引用该局部值

locals {
  test_var1 = "local test1"
  test_var2 = "local test2"
}

output "local_var" {
  value = local.test_var1       # 注意使用时是 local 而不是 locals
}

data 数据源

需要从其它地方查询数据的情况

  1. 从 remote state 获取
  2. 使用数据源查询 - 这种方式是从云 api 获取的数据
data "aws_ami" "example" {
  most_recent = true

  owners = ["self"]
  tags = {
    Name   = "app-server"
    Tested = "true"
  }
}

output "xxxx" {
  value = data.aws_ami.example.outputs.id
}
// 查询名称是GA的分布式交换机
data "vsphere_distributed_virtual_switch" "vds" {
  name          = "GA"
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

// 查询名称是NET1的分布式交换下端口组
data "vsphere_network" "network" {
  name          = "NET1"
  distributed_virtual_switch_uuid =  data.vsphere_distributed_virtual_switch.vds.id
  datacenter_id = data.vsphere_datacenter.datacenter.id
}

跨 State 状态文件共享数据

读取另外一个 tf 项目状态文件的输出; 注意另一个项目需要定义好对应的 output;

因为 output 的内容实际上是存储在状态文件中的, 所以远程调用时, 实际是从文件里面获取的数据内容;

data "terraform_remote_state" "network" {
  backend = "s3"
  config = {
    bucket = "my-tf-state"
    key    = "network/terraform.tfstate"
    region = "us-west-2"
  }
}

output "vpc_id" {
  value = data.terraform_remote_state.network.outputs.vpc_id
}

数据源查询

实际上是云 saas 接口查询的数据

# 查询 VPC 信息
data "alicloud_vpcs" "default" {
  ids = [data.terraform_remote_state.pro.outputs.vpc_id]
}

# 然后在规则中使用
cidr_ip = data.alicloud_vpcs.default.vpcs[0].cidr_block

aws 常用示例

# 查询当前账号信息
data "aws_caller_identity" "current" {}

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


# 查询可用区列表
data "aws_availability_zones" "available" {
  state = "available"
}

# 查询最新 Amazon Linux 2 AMI
data "aws_ami" "amazon_linux_2" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

# 查询私有子网 ID
data "aws_subnet_ids" "private" {
  vpc_id = var.vpc_id

  tags = {
    Type = "private"
  }
}

http查询

通过 http 协议去获取一个内容

https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http

data "http" "example" {
  url = "https://checkpoint-api.hashicorp.com/v1/check/terraform"
  method = "HEAD"       # GET, POST

  request_headers = {
    Accept = "application/json"
  }

  request_body = "request body"

  lifecycle {
    postcondition {
      condition     = contains([201, 204], self.status_code)
      error_message = "Status code invalid"
    }
  }

  ca_cert_pem = ""
  insecure = true
  # request_timeout_ms = 1000
}

output

资源的输出值, 也是 可导出属性, 可在 Terraform 配置的其他地方被引用 输出值类似于编程语言中的返回值;

root 模块引用子模块内容时, 必须要子模块定义输出

sensitive 特别说明

  1. 不在 console 输出
  2. 不在 log 日志中
  3. 在 plan 计划中
  4. 在 状态文件中
output "vswitch_id" {
  value = alicloud_vswitch.main_vswitch.id
  description = "描述"
  sensitive = true      # 是否敏感信息
  depends_on = []       # 依赖关系

  ephemeral   = true    # 临时值, 不要持久化到状态文件

  # 断言条件
  precondition = ""
}

查看敏感输出

terraform output -json | jq '.db_password.value' -r

内部资源

random_id

生成唯一后缀以避免存储桶名称冲突

resource "random_id" "suffix" {
  byte_length = 4
}


output "my_func1" {
  value = {
    data = ${random_id.suffix.hex}
  }
}

local_file 本地文件

resource "local_file" "example" { content = "Hello Terraform" filename = "${path.module}/hello.txt" }

moved 移动资源, 即重命名

只会修改本地状态

moved {
    from = <old address for the resource>
    to = <new address for the resource>
}

removed

从 tf 文件中删除资源, 但不希望删除云上的资源时, 标记需要删除状态

removed {
  from = aws_instance.example
  lifecycle {
    destroy = false
  }
}
removed {
  from = "<resource.address>"       # 指定了你想移除的资源地址
  lifecycle {
    destroy = < true || false >     # false, 在不实质删除的情况下移除托管
  }
  connection {
    <connection-settings>
  }
  provisioner "<TYPE>" {
    when = destroy
    <provisioner-type-arguments>
  }
}

can 与 try 错误处理

用于优雅处理 可能的出错表达式

can: 成功时返回 true;失败时返回 false 做条件判断,根据表达式是否成功来决定后续逻辑 不抛出错误,只返回布尔值供你判断

try: 成功时返回表达式的值;失败时返回你指定的默认值 为可能失败的表达式提供后备值,让配置继续执行 静默失败,使用你给的默认值

try(表达式1, 表达式2, ...) 会按顺序尝试执行所有表达式,并返回第一个成功执行的表达式的结果

can 示例

locals {
  server_data = {
    name = "web-server"
    # disk = "100GB"
  }

  has_disk = can(local.server_data.disk)    # 检查磁盘信息是否存在
}

# 只有当 has_disk 为 true 时,才创建磁盘资源
resource "alicloud_disk" "example" {
  count = local.has_disk ? 1 : 0
  name  = "data-disk"
  size  = local.server_data.disk
}

# 输入验证
variable "env" {
  type = string
  validation {
    condition     = can(regex("^(dev|staging|prod)$", var.env))
    error_message = "环境必须是 dev/staging/prod"
  }
}

try 示例

locals {
  instance_info = {
    id = "i-123456"
    # region = "us-east-1"  # 假设这个字段不存在!
  }

  # 使用 try 安全地获取 region, 如果不存在则使用 "unknown"
  region = try(local.instance_info.region, "unknown")
}

variable "instance_type" {
  default = "t2.micro"
}

output "safe_lookup" {
  value = try(var.instance_type, "t3.micro")    # 如果没有定义则为 t3.micro
}

Secrets敏感信息处理

输出时标记此为敏感信息, 注意状态文件内还有明文

output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

使用 AWS Secrets Manager 或 HashiCorp Vault 存储敏感数据

data "aws_secretsmanager_secret_version" "db_creds" {
  secret_id = "prod/db-password"
}

resource "aws_db_instance" "main" {
  # 从 Secrets Manager 获取密码, 而非明文存储
  password = data.aws_secretsmanager_secret_version.db_creds.secret_string
}

ephemeral 方式管理

ephemeral "vault_kv_secret_v2" "proxmox_auth" {
  mount = "homelab"
  name  = "infra/proxmox/local-cluster/auth"
}
最后更新于