核心对象
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"
}
|
HCL
HCL(HashiCorp Configuration Language)作为核心语言
配置文件支持两种格式:
- HCL(HashiCorp Configuration Language), 文件名以 .tf 结尾
- JSON, 文件名以 .tf.json 结尾
表达式
条件表达式
1
|
condition ? true_val : false_val
|
for 表达式
1
2
|
[ for VAR in OBJECT : VAR]
[ for INDEX, VAR in OBJECT: "${INDEX}=${VAR}"]
|
for 表达式创建对象
1
2
3
4
5
|
output "vpc_ids" {
value = {
for k, v in alicloud_vpc.example : k => v.id # 这个 => 就是赋值的意思
}
}
|
splat 表达式
比用 for 表达式更简洁的循环方式
内置函数
Terraform 语言包括许多内置函数, 可以从表达式中调用这些函数来转换;
function(arg1,arg2)
数值类
计算函数
max(12, 54, 3)
min(12, 54, 3)
ceil(5.2) –> 6 # 向上取整
floor(4.5) –> 4 # 向下取整
pow(3, 2) –> 9 # 计算指数幂
abs(number)
log(16, 2) –> 4 # 除
parseint() # 进制转换, 解析字符串数字 为对应进制的整数
parseint(“100”, 10)
parseint(“FF”, 16) –> 255
parseint(“aA”, 62) –> 656
示例
1
2
3
4
5
6
|
output "my_func1" {
value = {
f1 = [max(1, 2, 3), min(7, 8, 9), parseint("111", 2)],
f2 = [max([1, 2, 3]...), min([7, 8, 9]...), parseint("111", 2)]
}
}
|
字符串
format() 按指定格式拼接字符串 format(“Hello, %s!”, “Terraform”)
lower()
upper()
title(“hello world”) # 首字母大写
join() 用分隔符将字符串列表拼接为一个字符串; join(", “, [“a”, “b”, “c”])
split() 将字符串按分隔符拆分为列表
substr() 从字符串中截取子串 substr(“hello world”, 0, 5)
replace() 替换字符串中的内容
chomp(“hello\r\n”) –> hello
endswith(string, suffix)
trimspace(“a b c”) –> “abc” # 清理空格
trimprefix(“dev-server”, “dev-”) –> server # 清除前面的字符
trimsuffix(“user.txt”, “.txt”) –> user # 清除后面的字符
1
2
3
4
5
6
7
8
9
|
output "my_func2" {
value = {
f1 = format("Hello %s!", "anliven"),
f2 = join(":", ["name", "age", "title"]),
f3 = replace("a=b=c", "=", "–"),
f4 = split(",", "name,age,title")
}
}
|
set-map
element() 通过索引获取列表中的某个元素 element([“a”, “b”, “c”], 1)
length() 获取列表、映射或字符串的长度 length([“a”, “b”, “c”])
lookup(map,key,default) 在映射中查找键的值,若不存在则返回默认值 lookup({a=“A”, b=“B”}, “c”, “C”)
keys() 返回映射的所有键组成的列表 keys({a=1, b=2})
values(map) 创建map中value的list
merge(map1,map2,map3) 合并多个映射或对象, 相同key会被最后覆盖 merge({a=1}, {b=2})
flatten() 将嵌套的列表展开为单层列表 flatten([[“a”], [“b”, “c”]])
alltrue() 判断元素中都是 true
anytrue() 判断元素中存在 true
chunklist(list, size) 按照指定size拆分list
compact(list) 去除list中的空元素
concat(list1, list2) 合并两个list
contains(list, value) 判断元素是否存在list中
distinct(list) 去除list中的重复元素
list[index] 根据index获取list中的元素
index(list, value) 返回value元素的index
sort([“z”, “b”, “a”]) # 排序
toset() # 将字符串列表转换为集合, 会丢弃 顺序 并删除重复元素
示例
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
|
locals {
list1 = [1, 2, 3]
list2 = [7, 8, 9, null]
list3 = ["a", "b", "c", "a", "b", "c"]
}
//内置Collection集合函数
output "my_func3" {
value = {
p1 = alltrue([true, false]) //判断元素中都是true
p2 = anytrue([]) //判断元素中存在true
p3 = chunklist(["a", "b", "c"], 2) //按照指定size拆分list
p4 = compact(local.list2) //去除list中的空元素
p5 = concat(local.list1, local.list2) //合并两个list
p7 = distinct(local.list3) //去除list中的重复元素
p8 = element(local.list3, 0) //根据index获取list中的元素
p9 = flatten([[["a", "b"], []], ["c"]]) //将多层list转换成单list
p10 = index(local.list3, "c") //返回value元素的index, 返回第一个匹配的索引
p11 = length({ "a" = 1, "b" = 2 }) //计算string、list、map的长度
p12 = lookup({ a = "111", b = "222" }, "c", "what?") //检索map的key,不存在返口default
p13 = merge({ a = "b", c = "d" }, { e = "f", c = "z" }) //合并map,相同key会被最后覆盖
p14 = keys({ a = 1, c = 2, d = 3 }) //创建map中key的list
p15 = values({ a = 3, c = 2, d = 1 }) //创建map中value的list
p6 = contains(local.list2, 9) # 判断元素是否存在list中
p16 = contains(["us-east-1", "us-west-2"], var.region)
}
}
|
正则
regex() # regex("[0-9]+”, “env42-prod”)
regexall() # 多次
编码和加解密
base64decode()
base64encode()
base64gzip()
csvdecode
jsondecode
jsonencode
textdecodebase64
textencodebase64
urlencode
yamldecode
yamlencode
brypt
bash64sha256
bash64sha512
filebase64sha256
filemd5
filesha1
filesha256
md5
sha1
sha256
sha512
uuid
uuidv5
路径和文件名
bashpath
dirname
pathexpand
basename
file() 读取文件的内容 value = file("${path.module}/config.json")
fileexists
fileexists
filebase64
templatefile() 动态渲染模板
templatefile
1
2
3
4
5
6
7
8
9
10
11
|
nginx.conf.tmpl 原始文件内容
server {
listen 80;
server_name ${domain};
}
output "nginx_config" {
value = templatefile("${path.module}/nginx.conf.tmpl", {
domain = "example.com" # 变量渲染
})
}
|
时间
formatdate
plantimestamp
timeadd
timecmp
timestamp
ip
cidrhost
cidrnetmask
cidrsubnet() # 子网计算
cidrsubnets()
cidrsubnet 从 CIDR 生成子网
cidrsubnet(原始网段, 掩码, 子网步长)
default = cidrsubnet(“172.20.0.0/16”, 8, 36) # 最终等于 172.20.36.0/24
理解
- 172.20.0.0/16
- 16+8=24
- 将 “172.20.0.0/16” 拆成 24 个子网
- 使用第 36 个子网
cidrsubnet(“10.0.0.0/16”, 8, 1) –> “10.0.1.0/24”
cidrsubnet(“10.0.0.0/16”, 8, 2) –> “10.0.2.0/24”
技巧
展开参数 …
把列表中的值作为多个参数传给函数时,可以在列表后加 …
1
2
|
# 等同于 min(1, 2, 3)
min([1, 2, 3]...)
|