data 数据源
数据源 vs 资源
resource 创建和管理新资源;data source 查询现有资源的信息(只读)。数据源常用于:查询最新 AMI ID、获取已有 VPC 信息、读取 Secrets Manager 中的密钥。
# 查询最新的 Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
# 引用数据源:data.类型.名称.属性
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
}
# 从 Secrets Manager 读取密钥(不入 state)
data "aws_secretsmanager_secret_version" "db_pass" {
secret_id = "prod/db/password"
}
resource "aws_db_instance" "main" {
password = data.aws_secretsmanager_secret_version.db_pass.secret_string
}
count vs for_each
# for_each 示例
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "charlie"])
name = each.value
}
# map 形式(key 和 value 不同)
resource "aws_s3_bucket" "by_env" {
for_each = {
dev = "us-east-1"
prod = "us-west-2"
}
bucket = "my-${each.key}-${each.value}"
}
# 引用:aws_s3_bucket.by_env["prod"].id
# count 条件创建(开关模式)
resource "aws_cloudwatch_metric_alarm" "cpu" {
count = var.enable_alarms ? 1 : 0
alarm_name = "high-cpu"
# ...
}
dynamic 块
variable "ingress_ports" {
default = [
{ port = 80, cidr = ["0.0.0.0/0"] },
{ port = 443, cidr = ["0.0.0.0/0"] },
]
}
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = var.vpc_id
dynamic "ingress" {
for_each = var.ingress_ports
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = ingress.value.cidr
}
}
}
for 表达式与常用函数
locals {
# 列表转 map
users_by_id = { for u in var.users : u.id => u.name }
# 过滤列表
active_users = [for u in var.users : u if u.active]
# 常用函数
all_ids = flatten([module.a.ids, module.b.ids])
merged = merge(local.defaults, var.overrides)
unique = toset(var.regions)
max_count = max(1, var.count)
}
null_resource 配合 local-exec provisioner 可以在 Terraform 中执行本地脚本,常用于无法用 Provider 资源表达的操作,如调用外部 API 或触发 CI 流水线。尽量少用,优先找专用 Provider。
数据源的深度用法
跨模块数据共享:terraform_remote_state
当基础设施拆分为多个 Terraform 项目时(如 VPC 和应用分别管理),可以用 terraform_remote_state 读取其他项目的 output 值,实现跨项目数据共享。
# 场景:应用层项目读取网络层项目的 VPC/Subnet 信息
# 在网络层项目中(terraform/network/outputs.tf)
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = [for s in aws_subnet.private : s.id]
}
# 在应用层项目中(terraform/app/main.tf)
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-company-terraform-state"
key = "network/terraform.tfstate" # 网络层的 state 路径
region = "us-east-1"
}
}
# 读取网络层的输出值
resource "aws_ecs_service" "app" {
network_configuration {
# 直接引用网络层定义的子网
subnets = data.terraform_remote_state.network.outputs.private_subnet_ids
}
}
resource "aws_security_group" "app" {
# 直接引用网络层的 VPC ID
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
}
使用 terraform_remote_state 会创建隐式的项目间依赖:如果网络层修改了 output 的结构,应用层可能会受影响。替代方案:使用 AWS Parameter Store 或 SSM 存储共享值(更松耦合,不需要读取其他项目的 state 权限)。
常用数据源速查
# 当前 AWS 账号信息
data "aws_caller_identity" "current" {}
# 使用:data.aws_caller_identity.current.account_id
# 当前区域信息
data "aws_region" "current" {}
# 使用:data.aws_region.current.name → "us-east-1"
# 查询已有 VPC(不是 Terraform 管理的也可以)
data "aws_vpc" "existing" {
filter {
name = "tag:Name"
values = ["production-vpc"]
}
}
# 查询所有可用区(自动适应不同区域)
data "aws_availability_zones" "available" {
state = "available"
}
# 使用:data.aws_availability_zones.available.names
# → ["us-east-1a", "us-east-1b", "us-east-1c"]
# 查询 ACM 证书
data "aws_acm_certificate" "api" {
domain = "api.example.com"
statuses = ["ISSUED"]
}
# 查询 Route53 Zone ID
data "aws_route53_zone" "main" {
name = "example.com"
private_zone = false
}
count 与 for_each 的陷阱
count 的删除重建问题
这是 Terraform 初学者最容易踩的坑之一:
# 危险!使用 count 创建 3 个子网
resource "aws_subnet" "private" {
count = 3
cidr_block = cidr.subnet("10.0.0.0/16", 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
}
# State 中:aws_subnet.private[0], [1], [2]
# 如果删除 AZ "a"(count 变为 2):
# Terraform 会删除 [2](而不是 [0]!),因为它按索引管理
# 这可能删错 subnet,导致服务中断
# ✅ 正确:使用 for_each + 可用区名称作为 key
resource "aws_subnet" "private" {
for_each = toset(data.aws_availability_zones.available.names)
cidr_block = # ... 根据 AZ 名称计算 CIDR
availability_zone = each.key
}
# State 中:aws_subnet.private["us-east-1a"], ["us-east-1b"], ["us-east-1c"]
# 删除 "us-east-1a" 只删除 ["us-east-1a"],其他不受影响
for_each 的值必须在 plan 时已知
# ❌ 错误:for_each 的值依赖于另一个资源的运行时输出
# 因为 Terraform 在 plan 阶段不知道 aws_instance.ids 的值
resource "aws_eip" "web" {
for_each = aws_instance.web # ← 如果 web 是用 for_each 创建的,这是可以的
instance = each.value.id # ← 但如果依赖动态数量的资源则可能报错
}
# ✅ 正确:for_each 的 key 来自静态定义的 map/set
locals {
servers = {
web1 = { type = "t3.medium", az = "us-east-1a" }
web2 = { type = "t3.medium", az = "us-east-1b" }
}
}
resource "aws_instance" "web" {
for_each = local.servers
instance_type = each.value.type
availability_zone = each.value.az
}
条件表达式与条件资源
三元条件表达式
HCL 的条件表达式语法:condition ? true_val : false_val,与大多数编程语言相同。
# 条件表达式的各种用法
locals {
# 根据环境选择实例类型
instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
# 多层嵌套条件(类似 if-elseif-else)
db_class = (
var.environment == "prod" ? "db.r6g.xlarge" :
var.environment == "staging" ? "db.t3.medium" :
"db.t3.micro"
)
# 可选属性(当条件为 false 时使用 null)
# Terraform 会自动忽略 null 的属性
kms_key = var.enable_encryption ? aws_kms_key.main.arn : null
}
# 条件资源(用 count = 0/1 作为开关)
resource "aws_cloudwatch_log_group" "app" {
# 只在 dev 和 prod 创建,staging 用共享 log group
count = var.environment != "staging" ? 1 : 0
name = "/app/${var.environment}"
retention_in_days = var.environment == "prod" ? 90 : 7
}
# 引用可选资源(要用 one() 函数)
locals {
# one() 将 list(0 or 1 elements) 转为单个值或 null
log_group_name = one(aws_cloudwatch_log_group.app[*].name)
}
Terraform 内置函数完全参考
字符串函数
locals {
# format:格式化字符串(类似 printf)
bucket_name = format("my-company-%s-%s", var.environment, var.region)
# join/split:字符串连接和分割
tags_string = join(",", ["env=prod", "team=platform"])
parts = split(".", "example.com") # → ["example", "com"]
# replace:字符串替换
safe_name = replace(var.name, "/", "-") # 替换斜杠为连字符
# trim/trimspace:去除空格
clean = trimspace(" hello ") # → "hello"
# lower/upper:大小写转换
env_lower = lower(var.environment) # 统一小写
}
集合函数
locals {
# flatten:展开嵌套列表
all_subnet_ids = flatten([
module.vpc_a.subnet_ids,
module.vpc_b.subnet_ids,
])
# concat:合并多个列表
all_tags = concat(var.common_tags, var.app_tags)
# merge:合并多个 map(后者覆盖前者)
final_config = merge(local.defaults, var.overrides)
# distinct:去重
unique_regions = distinct(var.regions)
# length:列表/map 长度
subnet_count = length(var.subnet_ids)
# contains:检查元素是否存在
is_prod = contains(["prod", "production"], var.environment)
# keys/values:从 map 提取键或值
env_names = keys(var.environments)
# zipmap:用两个列表创建 map
user_map = zipmap(var.user_names, var.user_roles)
# lookup:安全地从 map 取值(不存在时返回默认值)
instance_type = lookup(var.instance_sizes, var.environment, "t3.micro")
}
网络/编码函数
locals {
# cidrsubnet:从 VPC CIDR 计算子网 CIDR
# cidrsubnet(prefix, newbits, netnum)
# newbits:在原前缀长度基础上增加的位数
subnet_a = cidrsubnet("10.0.0.0/16", 8, 0) # → "10.0.0.0/24"
subnet_b = cidrsubnet("10.0.0.0/16", 8, 1) # → "10.0.1.0/24"
subnet_c = cidrsubnet("10.0.0.0/16", 8, 2) # → "10.0.2.0/24"
# cidrhost:计算 CIDR 块内的特定主机 IP
gateway_ip = cidrhost("10.0.0.0/24", 1) # → "10.0.0.1"(第一个主机)
# base64encode/decode:Base64 编解码
encoded_data = base64encode("Hello, Terraform!")
# jsonencode/jsondecode:JSON 序列化
policy_json = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = "*"
}]
})
}
null_resource 与 terraform_data
hashicorp/null provider。local-exec(在运行 Terraform 的机器上执行)和 remote-exec(通过 SSH/WinRM 在目标机器上执行)。Terraform 官方不推荐在生产中使用 provisioner,应优先使用 user_data、cloud-init 或配置管理工具(Ansible)。null_resource 实战案例
# 引入 null provider(Terraform 1.4 之前必须)
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
# 案例1:EC2 创建后执行初始化脚本
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
key_name = aws_key_pair.deploy.key_name
# user_data 推荐用于实例启动初始化(比 remote-exec 更可靠)
user_data = base64encode(<<-EOF
#!/bin/bash
apt-get update -y
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
EOF)
}
# 案例2:等待 EC2 可 SSH 后执行配置
resource "null_resource" "configure_web" {
# triggers 决定何时重新执行:instance_id 不变则不重新运行
triggers = {
instance_id = aws_instance.web.id
# 脚本内容变化时也重新运行
script_hash = filemd5("${path.module}/scripts/configure.sh")
}
# SSH 连接配置
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/deploy_key")
host = aws_instance.web.public_ip
}
# 上传文件到远程机器
provisioner "file" {
source = "scripts/configure.sh"
destination = "/tmp/configure.sh"
}
# 在远程机器执行命令
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/configure.sh",
"sudo /tmp/configure.sh",
]
}
# 依赖:确保 EC2 创建完成后才执行
depends_on = [aws_instance.web]
}
# 案例3:apply 完成后发送通知(local-exec)
resource "null_resource" "notify_deploy" {
triggers = {
# 每次 apply 都触发(timestamp 每次都变)
always_run = timestamp()
}
provisioner "local-exec" {
# 在本地运行:调用 Slack Webhook 发通知
command = <<-EOF
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"✅ 部署完成!EC2 IP: ${aws_instance.web.public_ip}"}' \
${var.slack_webhook_url}
EOF
}
# 销毁时也发通知
provisioner "local-exec" {
when = destroy
command = "echo '⚠️ 资源即将销毁'"
}
}
terraform_data 替代 null_resource(推荐)
# Terraform 1.4+ 内置,无需 null provider
resource "terraform_data" "bootstrap_db" {
# input 存储任意数据,变化时触发重建
input = {
db_endpoint = aws_db_instance.main.endpoint
schema_hash = filemd5("${path.module}/sql/schema.sql")
}
provisioner "local-exec" {
# self.input 引用上面的 input 值
command = <<-EOF
psql "postgresql://admin:${var.db_password}@${self.input.db_endpoint}/mydb" \
-f ${path.module}/sql/schema.sql
EOF
}
}
# terraform_data 还可以作为触发器
# 当 ami_id 变化时强制重建依赖它的资源
resource "terraform_data" "ami_version" {
input = data.aws_ami.ubuntu.id # 存储当前 AMI ID
}
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
lifecycle {
# 当 AMI 版本数据变化时,触发实例替换
replace_triggered_by = [terraform_data.ami_version]
}
}
- provisioner 只在创建/销毁时执行一次:后续 apply 不会再次运行(除非 triggers 变化)。不能用于持续配置管理。
- provisioner 失败会导致资源变为 tainted 状态:下次 apply 会强制替换该资源,可能导致数据丢失。
- remote-exec 需要网络连通:EC2 需要有公网 IP 或 VPN 才能 SSH,增加安全面。生产环境应避免。
- 推荐替代方案:
- 实例配置:用
user_data+ cloud-init(简单)或 Ansible/Chef(复杂) - 数据库初始化:用 CI/CD pipeline 在 Terraform apply 后单独执行
- 通知:用 CI/CD pipeline 的 post-step 发送
- 实例配置:用
time_sleep 与外部数据源
time provider:等待资源就绪
terraform {
required_providers {
time = {
source = "hashicorp/time"
version = "~> 0.9"
}
}
}
# 在 EKS 集群创建后等待 30 秒再安装 add-ons
# (某些 AWS 服务需要 "传播时间")
resource "time_sleep" "wait_for_eks" {
depends_on = [aws_eks_cluster.main]
create_duration = "30s"
}
resource "aws_eks_addon" "coredns" {
cluster_name = aws_eks_cluster.main.name
addon_name = "coredns"
# 明确依赖等待资源,确保 add-on 在等待后安装
depends_on = [time_sleep.wait_for_eks]
}
# time_rotating:生成周期性变化的时间戳(如强制定期轮换密钥)
resource "time_rotating" "api_key_rotation" {
rotation_days = 30 # 每 30 天"到期",触发依赖重建
}
resource "aws_api_gateway_api_key" "main" {
name = "my-api-key"
lifecycle {
replace_triggered_by = [time_rotating.api_key_rotation]
}
}
external data source:调用任意外部程序
terraform {
required_providers {
external = {
source = "hashicorp/external"
version = "~> 2.0"
}
}
}
# external data source 执行一个外部程序并读取其 JSON 输出
# 程序必须从 stdin 读取 JSON 输入,向 stdout 输出 JSON 对象
data "external" "instance_types" {
program = ["python3", "${path.module}/scripts/get_instance_types.py"]
query = {
region = var.aws_region
min_vcpus = "4"
min_memory = "8192"
}
}
# 引用外部程序输出的 JSON 字段
output "best_instance_type" {
value = data.external.instance_types.result.instance_type
}
外部程序必须是幂等的(多次调用返回相同结果),因为 Terraform 可能在 plan 和 apply 阶段各调用一次。外部程序不能有副作用(不能写文件、不能调用 API 创建资源)。如果需要有副作用的操作,应使用 null_resource + local-exec 或 terraform_data。
depends_on 与依赖管理
resource_b.name),Terraform 自动推断 A 依赖 B,无需手动声明。这是 Terraform 依赖管理的主要方式。depends_on 手动声明。例如:IAM 策略附加后需要等待权限传播,才能创建使用该权限的资源;数据库实例创建完成后才能运行初始化脚本。terraform graph 命令可输出 DOT 格式的依赖图,用 Graphviz 可视化。# 隐式依赖示例:subnet 引用了 vpc 的属性
# Terraform 自动知道先创建 vpc,再创建 subnet
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # 隐式依赖:引用了 vpc 的 id
cidr_block = "10.0.1.0/24"
}
# 显式依赖示例:IAM 权限传播问题
resource "aws_iam_role_policy_attachment" "s3_policy" {
role = aws_iam_role.lambda_role.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
}
resource "aws_lambda_function" "processor" {
function_name = "data-processor"
role = aws_iam_role.lambda_role.arn
# 即使 role ARN 已知,AWS IAM 权限传播需要时间
# 显式声明依赖,确保策略附加完成后再创建 Lambda
depends_on = [aws_iam_role_policy_attachment.s3_policy]
}
# 可视化依赖图(需要安装 graphviz)
# terraform graph | dot -Tpng > graph.png
对 data source 使用 depends_on 会强制它在 apply 阶段才执行查询(而非 plan 阶段),导致某些属性在 plan 阶段显示为 (known after apply),降低 plan 的预测性。如非必要,应避免对 data source 使用 depends_on。
terraform console:交互式调试
terraform console 提供了一个交互式 REPL 环境,可以在不执行 apply 的情况下测试表达式、函数和 State 数据,是调试 HCL 的利器。
# 启动 terraform console(需要先 init)
terraform console
# 在 console 中测试函数
> cidrsubnet("10.0.0.0/16", 8, 0)
"10.0.0.0/24"
> cidrsubnet("10.0.0.0/16", 8, 1)
"10.0.1.0/24"
# 测试字符串操作
> format("%-10s = %s", "region", "us-east-1")
"region = us-east-1"
> join(", ", ["web", "api", "db"])
"web, api, db"
> split(",", "web,api,db")
tolist([
"web",
"api",
"db",
])
# 测试 for 表达式
> [for s in ["a", "b", "c"] : upper(s)]
tolist([
"A",
"B",
"C",
])
# 测试 jsonencode
> jsonencode({a = 1, b = ["x", "y"]})
"{\"a\":1,\"b\":[\"x\",\"y\"]}"
# 读取当前 State 中的资源属性(需要先 apply)
> aws_vpc.main.id
"vpc-0123456789abcdef0"
> aws_vpc.main.cidr_block
"10.0.0.0/16"
# 测试局部变量(如果在 .tf 文件中定义了 locals)
> local.common_tags
{
"Environment" = "dev"
"ManagedBy" = "Terraform"
}
# 退出 console
> exit
# 或按 Ctrl+D
- 调试 cidrsubnet:在写 VPC 子网配置前,先在 console 中验证 CIDR 计算结果是否符合预期。
- 调试 for 表达式:复杂的 list/map 转换逻辑先在 console 中测试。
- 查看 State:apply 后用 console 快速查看特定资源的属性值,比
terraform show更精准。 - 测试 jsonencode:写 IAM 策略前先验证 JSON 结构正确。
本章小结
- data source 是只读数据查询:引用格式为
data.类型.名称.属性;常用于查询最新 AMI、获取当前账号 ID、读取 Secrets Manager 密钥;terraform_remote_state可跨项目读取 output。 - for_each 优于 count:count 基于索引,删除中间元素会导致后续资源重建(可能误删);for_each 基于稳定 key,增删某个元素不影响其他资源;for_each 的 key 集合必须在 plan 阶段已知。
- dynamic 块解决了需要动态生成嵌套块(如安全组规则)的问题;
content块内通过迭代器.value.字段访问当前元素。 - 条件表达式:
condition ? true : false;条件资源用count = 0/1控制;引用可选资源用one(resource[*].attr)。 - null_resource / terraform_data:用于执行 provisioner(本地或远程命令);terraform_data 是 Terraform 1.4+ 内置的替代品,无需额外 provider;triggers 控制何时重新执行。
- depends_on 用于隐式依赖无法覆盖的场景:如 IAM 权限传播;对 data source 使用 depends_on 会推迟其执行到 apply 阶段,影响 plan 预测性。
- terraform console 是调试利器:可在不 apply 的情况下测试 HCL 表达式、函数计算结果和 State 数据。
- 内置函数:字符串(format/join/replace)、集合(flatten/merge/lookup)、网络(cidrsubnet/cidrhost)、编码(jsonencode/base64encode)是最常用的函数类别。