Resource 与 Data Sources
核心问题:怎样声明要创建的云资源?怎样引用已存在(不由当前 Terraform 管理)的资源?资源之间怎样互相引用?
Resource 块
resource 是 Terraform 的核心——声明你想要的云资源:
resource "<资源类型>" "<本地名称>" {
<参数> = <值>
...
}
- 资源类型:由 Provider 决定,如
aws_instance、google_compute_instance - 本地名称:在当前模块内唯一,用于引用
- 引用格式:
<资源类型>.<本地名称>.<属性>
resource "aws_security_group" "web" {
name = "${local.name_prefix}-web-sg"
description = "Web server security group"
vpc_id = aws_vpc.main.id # 引用同模块中其他资源的属性
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
资源依赖
隐式依赖(推荐)
通过引用其他资源的属性自动建立依赖,Terraform 计算依赖图,自动决定创建顺序:
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.medium"
subnet_id = aws_subnet.public[0].id # 隐式依赖 aws_subnet
vpc_security_group_ids = [aws_security_group.web.id] # 隐式依赖 aws_security_group
tags = local.common_tags
}
Terraform 会自动先建 aws_subnet 和 aws_security_group,再建 aws_instance。
显式依赖(depends_on)
当依赖关系无法通过属性引用表达时使用:
resource "aws_ecs_service" "app" {
name = "my-app"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.app.arn
# IAM 策略绑定无法通过属性引用,需要显式声明
depends_on = [
aws_iam_role_policy_attachment.task_execution_policy
]
}
count:创建多个相同资源
resource "aws_instance" "web" {
count = var.instance_count # 创建 N 个
ami = data.aws_ami.ubuntu.id
instance_type = local.instance_type
subnet_id = aws_subnet.public[count.index % length(aws_subnet.public)].id
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index + 1}"
})
}
# 引用:aws_instance.web[0].public_ip
# 引用所有:aws_instance.web[*].public_ip(splat 表达式)
for_each:按 Map/Set 创建
for_each 比 count 更灵活——资源有独立 key,增删某项不影响其他项:
variable "buckets" {
type = map(object({
acl = string
versioning = bool
}))
default = {
media = { acl = "public-read", versioning = true }
backups = { acl = "private", versioning = true }
logs = { acl = "private", versioning = false }
}
}
resource "aws_s3_bucket" "buckets" {
for_each = var.buckets
bucket = "${local.name_prefix}-${each.key}"
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-${each.key}"
})
}
resource "aws_s3_bucket_acl" "buckets" {
for_each = var.buckets
bucket = aws_s3_bucket.buckets[each.key].id
acl = each.value.acl
}
# 引用:aws_s3_bucket.buckets["media"].bucket
Data Sources
data 块查询已存在的资源(不创建),用于引用:
- 不由当前 Terraform 管理的资源(手动创建的 VPC、其他团队的 Terraform State)
- 动态查询(最新 AMI、可用区列表)
# 查询最新 Ubuntu 22.04 AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical 官方账号
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# 引用
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # 使用查到的 AMI ID
...
}
# 引用另一个 Terraform State 的输出(跨模块协作)
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-tf-state"
key = "prod/vpc/terraform.tfstate"
region = "ap-southeast-1"
}
}
# 引用其他 State 的输出
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.vpc.outputs.private_subnet_ids[0]
}
# 查询可用区
data "aws_availability_zones" "available" {
state = "available"
}
# 查询当前账号信息
data "aws_caller_identity" "current" {}
output "account_id" {
value = data.aws_caller_identity.current.account_id
}
# 查询已有 Route53 Hosted Zone
data "aws_route53_zone" "main" {
name = "example.com"
private_zone = false
}
resource "aws_route53_record" "api" {
zone_id = data.aws_route53_zone.main.zone_id
name = "api.example.com"
type = "A"
alias {
name = aws_lb.main.dns_name
zone_id = aws_lb.main.zone_id
evaluate_target_health = true
}
}
实战:完整 Web 应用基础设施
# 数据源
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# EC2 实例
resource "aws_instance" "web" {
count = var.web_instance_count
ami = data.aws_ami.ubuntu.id
instance_type = local.instance_type
subnet_id = aws_subnet.public[count.index].id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.web.name
key_name = var.key_name
root_block_device {
volume_type = "gp3"
volume_size = 20
delete_on_termination = true
encrypted = true
}
user_data = base64encode(templatefile("${path.module}/templates/user_data.sh.tpl", {
app_env = var.env
}))
tags = merge(local.common_tags, {
Name = "${local.name_prefix}-web-${count.index + 1}"
Role = "web"
})
lifecycle {
create_before_destroy = true # 蓝绿替换
ignore_changes = [ami] # AMI 更新不自动重建(由 Ansible 管理)
}
}
# ALB
resource "aws_lb" "main" {
name = "${local.name_prefix}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
access_logs {
bucket = aws_s3_bucket.logs.bucket
prefix = "alb"
enabled = true
}
tags = local.common_tags
}
lifecycle 元参数
resource "aws_instance" "web" {
...
lifecycle {
create_before_destroy = true # 先创建新资源再删旧的(零停机替换)
prevent_destroy = true # 防止意外 terraform destroy(生产数据库常用)
ignore_changes = [tags] # 忽略某些属性的外部变更
replace_triggered_by = [ # 当某依赖变更时强制替换
aws_launch_template.web.id
]
}
}
常见错误
| 错误 | 原因 | 解决 |
|---|---|---|
Error: Cycle: A → B → A | 资源循环依赖 | 检查 depends_on 和属性引用,找出环路 |
count.index 引发索引越界 | count 值小于引用的列表长度 | 用取模:[count.index % length(list)] |
for_each 值包含 null | null 不可用作 Map key | 用 compact() 或 { for k, v in map : k => v if v != null } 过滤 |
data source not found | 查询条件过严或资源不存在 | 放宽 filter 条件,或先确认资源存在 |