核心对象
resource 资源
data 数据源
locals 局部变量
output 输出和模块导出
variable 变量
ephemeral 临时变量
moved 移动资源, 即重命名
removed 只删除本地状态内的对象, 不影响云上资源
resource 资源
资源定义
“资源模块名 + 状态名称” 需要全局唯一
1
|
resource "资源模块名" "状态名称" {}
|
元参数
每种资源都可用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
resource "xxxxx" "xxxxx" {
# 显式声明依赖关系
depends_on: []
# 创建多个资源实例
count:
# 迭代集合, 为集合中每一个元素创建一个对应的资源实例
for_each:
# 指定非默认 Provider 实例
provider:
# 自定义资源的生命周期行为
lifecycle:
# 默认是在资源 创建时执行 执行一些额外的操作
provisioner:
在资源创建后执行一些额外的操作
connection:
}
|
如果创建的资源实例彼此之间几乎完全一致,那么 count 比较合适
如果多个资源参数差异较大, 那么使用 for_each 会更加安全合适
count
count 元参数接受一个整数, 并创建相应数量的资源或模块实例
- 一个资源或 Module 块不能同时使用 count 和 for_each
- 标号从 0 开始
示例: 创建4个主机
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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 条件式创建资源
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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 能够精确跟踪单个资源实例, 即使底层集合的顺序发生变化;
- 一个 resource 块不允许同时声明 count 与 for_each
- 不能直接使用列表(list), 需要转换为 set
each 对象
each.key — 对应于此实例的映射键(或集合成员); 必须是已知值, 不能是生成值;
each.value — 对应于此实例的映射值; (如果提供的是集合, 则与 each.key 相同; )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
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
1
2
3
|
locals {
vswitch_ids = toset(["vsw-abc123456", "vsw-def789012"])
}
|
lifecycle 生命周期
1
2
3
4
5
6
7
8
9
10
11
12
13
|
lifecycle {
# 忽略某些差异
ignore_changes = [
tags,
]
# 运行前校验
postcondition {
condition = self.tags["Component"] == "nomad-server"
error_message = "tags[\"Component\"] must be \"nomad-server\"."
}
}
|
provisioner
默认是资源创建时执行
1
2
3
4
5
6
|
resource "aws_instance" "server" {
provisioner "local-exec" {
when = destroy # 设定为资源销毁时执行
command = "echo 'Destroy-time provisioner'"
}
}
|
变量
变量类型
基础类型:
复合类型:
- list() 本身是有序的
- set() 一组不重复的值
- map() 键类型必须是 string;
- any: 任意类型
- object((ATTR_NAME = ATTR_TYPE, …))
- tuple([,…])
map 只是kv结构, object 才是嵌入结构
变量引用: var.VAR_NAME
变量传入和优先级
- 默认值 variable 中的 default 值
- 文件 *.tfvars, *.tfvars.json, *.auto.tfvars, *.auto.tfvars.json
- 有 TF_VAR_ 的名称开头的系统环境变量会进行关联
- 命令行参数, -var 选项传递变量, -var-file 选项加载一个变量文件
- 当没有默认值, 且没有传入时, 会要求手动输入
注意: 其它非 TF_VAR_ 的名称开头变量是供应商模块运行所需自行读取的, 与用户自定义变量无关;
1
2
3
4
5
6
|
-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
|
默认变量文件
1
2
3
4
5
6
|
.tfvars
.tfvars.json
terraform.tfvars
terraform.tfvars.json
.auto.tfvars
.auto.tfvars.json
|
variable 变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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 临时变量
针对敏感秘密的场景, 不进行任何形式的持久化;
- 不在 console 和 log 文件中
- 不在 plan 计划文件中
- 不在 状态文件中
- 只能在特定上下文中引用临时变量
locals 局部变量
通过 locals 关键字进行定义, 通过 local.VAR_NAME 调用
局部值有助于避免在配置中多次重复相同的值或表达式, 多用于调试和测试
场景:
用一个比较复杂的表达式计算某一个值,并且反复使用,
这时把这个复杂表达式赋予一个局部值,然后多次引用该局部值
1
2
3
4
5
6
7
8
|
locals {
test_var1 = "local test1"
test_var2 = "local test2"
}
output "local_var" {
value = local.test_var1 # 注意使用时是 local 而不是 locals
}
|
data 数据源
需要从其它地方查询数据的情况
- 从 remote state 获取
- 使用数据源查询 - 这种方式是从云 api 获取的数据
1
2
3
4
5
6
7
8
9
10
11
12
13
|
data "aws_ami" "example" {
most_recent = true
owners = ["self"]
tags = {
Name = "app-server"
Tested = "true"
}
}
output "xxxx" {
value = data.aws_ami.example.outputs.id
}
|
1
2
3
4
5
6
7
8
9
10
11
12
|
// 查询名称是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 的内容实际上是存储在状态文件中的, 所以远程调用时, 实际是从文件里面获取的数据内容;
1
2
3
4
5
6
7
8
9
10
11
12
|
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 接口查询的数据
1
2
3
4
5
6
7
|
# 查询 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 常用示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
# 查询当前账号信息
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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 特别说明
- 不在 console 输出
- 不在 log 日志中
- 在 plan 计划中
- 在 状态文件中
1
2
3
4
5
6
7
8
9
10
11
|
output "vswitch_id" {
value = alicloud_vswitch.main_vswitch.id
description = "描述"
sensitive = true # 是否敏感信息
depends_on = [] # 依赖关系
ephemeral = true # 临时值, 不要持久化到状态文件
# 断言条件
precondition = ""
}
|
查看敏感输出
1
|
terraform output -json | jq '.db_password.value' -r
|
内部资源
random_id
生成唯一后缀以避免存储桶名称冲突
1
2
3
4
5
6
7
8
9
10
|
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 移动资源, 即重命名
只会修改本地状态
1
2
3
4
|
moved {
from = <old address for the resource>
to = <new address for the resource>
}
|
removed
从 tf 文件中删除资源, 但不希望删除云上的资源时, 标记需要删除状态
1
2
3
4
5
6
|
removed {
from = aws_instance.example
lifecycle {
destroy = false
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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敏感信息处理
输出时标记此为敏感信息, 注意状态文件内还有明文
1
2
3
4
|
output "db_password" {
value = aws_db_instance.main.password
sensitive = true
}
|
使用 AWS Secrets Manager 或 HashiCorp Vault 存储敏感数据
1
2
3
4
5
6
7
8
|
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 方式管理
1
2
3
4
|
ephemeral "vault_kv_secret_v2" "proxmox_auth" {
mount = "homelab"
name = "infra/proxmox/local-cluster/auth"
}
|