You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Terraform动态创建AWS EFS挂载目标遇执行计划错误求助

问题描述

在Terraform部署流程中,VPC模块通过data "aws_availability_zones" "available"自动为目标区域的每个可用区创建公网和私有子网。但在EFS模块中尝试为每个私有子网创建挂载目标时,执行terraform plan报错:

Error: Invalid for_each argument
│ 
│   on modules/efs/main.tf line 55, in resource "aws_efs_mount_target" "efs_mount_targets_private":
│   XX:     for_each = toset(var.private_subnets)
│     ├────────────────
│     │ var.private_subnets is a list of string, known only after apply

尝试在EFS模块内添加aws_subnets数据块后问题依旧,需要找到无需分阶段部署的解决办法,同时疑惑:为什么基于aws_availability_zones创建子网可行,而EFS场景会触发这个错误?

相关代码片段如下:

根模块main.tf片段

module "vpc" {
  count                              = terraform.workspace == "backend" ? 0 : 1
  source                             = "./modules/vpc"

  vpc_flow_logs_to_cloud_watch_group = module.cw[0].my-vpc-flow-logs-group
  vpc_flow_logs_to_cloud_watch_role  = module.iam[0].vpc_flow_logs_to_cloud_watch_role
  vpn_vgw_id = module.vpn[0].vpn-vgw-id
  nat_gw_eip = module.ec2[0].nat_gw_eip
}

module "efs" {
  count = terraform.workspace == "backend" ? 0 : 1
  source = "./modules/efs"

  vpc_id = module.vpc[0].my_vpc_id
  allow-nfs-ingress-my-vpc = module.sg-ingress[0].allow-nfs-ingress-my-vpc-id
  private_subnets = module.vpc[0].my_vpc_private_subnets
}

VPC模块main.tf片段

# Create private subnets out of odd octets
resource "aws_subnet" "my_vpc_private_subnets" {
  for_each = toset(data.aws_availability_zones.available.names)

  vpc_id = aws_vpc.my_vpc.id
  availability_zone = each.value
  cidr_block = "172.25.${2*(index(data.aws_availability_zones.available.names, each.value))+1}.0/24"

  tags = {
    auto-delete = "no"
    Name = "${each.value} subnet ${2*(index(data.aws_availability_zones.available.names, each.value))+1}"
    tier = "private"
  }
}

data "aws_subnets" "private_subnets" {
  filter {
    name = "vpc-id"
    values = [aws_vpc.my_vpc.id]
  }

  tags = {
    tier = "private"
  }

  # Without the below, this data block might evaluate to empty
  depends_on = [
    aws_subnet.my_vpc_private_subnets
  ]
}

VPC模块outputs.tf片段

output "my_vpc_private_subnets" {
  value = data.aws_subnets.private_subnets.ids
}

EFS模块挂载目标代码

resource "aws_efs_mount_target" "efs_mount_targets_private" {
    for_each = toset(var.private_subnets)

    file_system_id = aws_efs_file_system.pgsql_databases.id
    security_groups = [ "${var.allow-nfs-ingress-my-vpc}" ]
    subnet_id = each.value
}

解决办法

核心问题是var.private_subnets的值是apply阶段才能生成的未知值,而for_each要求在plan阶段就明确知道集合的所有元素。以下两种方案可解决问题:

方案1:复用可用区列表,在EFS模块内查询子网

放弃直接传递子网ID列表,改为传递可用区列表,在EFS模块内通过可用区+标签筛选私有子网:

  1. 修改EFS模块变量定义:
variable "vpc_id" {
  type = string
}

variable "private_subnet_azs" {
  type = set(string)
}

variable "allow-nfs-ingress-my-vpc" {
  type = string
}
  1. 根模块传递参数:
module "efs" {
  count = terraform.workspace == "backend" ? 0 : 1
  source = "./modules/efs"

  vpc_id = module.vpc[0].my_vpc_id
  allow-nfs-ingress-my-vpc = module.sg-ingress[0].allow-nfs-ingress-my-vpc-id
  private_subnet_azs = module.vpc[0].my_vpc_azs # 需要在VPC模块新增该输出
}
  1. VPC模块添加可用区输出:
output "my_vpc_azs" {
  value = toset(data.aws_availability_zones.available.names)
}
  1. EFS模块内创建挂载目标:
data "aws_subnet" "private_subnets" {
  for_each = var.private_subnet_azs

  vpc_id = var.vpc_id
  availability_zone = each.value
  tags = {
    tier = "private"
  }
}

resource "aws_efs_mount_target" "efs_mount_targets_private" {
  for_each = data.aws_subnet.private_subnets

  file_system_id = aws_efs_file_system.pgsql_databases.id
  security_groups = [var.allow-nfs-ingress-my-vpc]
  subnet_id = each.value.id
}

方案2:直接传递VPC模块的子网资源映射

在VPC模块中输出子网资源的完整映射,而非通过数据块获取的ID列表:

  1. 修改VPC模块outputs.tf:
output "my_vpc_private_subnets_map" {
  value = aws_subnet.my_vpc_private_subnets
}
  1. 根模块传递参数:
module "efs" {
  count = terraform.workspace == "backend" ? 0 : 1
  source = "./modules/efs"

  vpc_id = module.vpc[0].my_vpc_id
  allow-nfs-ingress-my-vpc = module.sg-ingress[0].allow-nfs-ingress-my-vpc-id
  private_subnets_map = module.vpc[0].my_vpc_private_subnets_map
}
  1. EFS模块变量与挂载目标代码:
variable "private_subnets_map" {
  type = map(object({
    id = string
    availability_zone = string
  }))
}

resource "aws_efs_mount_target" "efs_mount_targets_private" {
  for_each = var.private_subnets_map

  file_system_id = aws_efs_file_system.pgsql_databases.id
  security_groups = [var.allow-nfs-ingress-my-vpc]
  subnet_id = each.value.id
}

场景差异原因解释
  • VPC模块创建子网时,for_each使用的data.aws_availability_zones.available.names是plan阶段就能确定的已知数据——Terraform可以提前查询AWS获取目标区域的可用区列表,因此能在plan阶段明确子网的创建数量和对应参数。
  • 而EFS模块中使用的var.private_subnets来自VPC模块的data.aws_subnets.private_subnets.ids,这个数据块依赖于aws_subnet.my_vpc_private_subnets资源,资源的ID只有在apply阶段创建完成后才会生成,属于plan阶段的未知值,不符合for_each的参数要求。

内容的提问来源于stack exchange,提问作者RiverRook

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.03 09:27:41