Terraform使用for_each引用远程状态报错的问题求助
解决Terraform for_each依赖远程状态导致的"known only after apply"问题
问题场景
使用Terraform搭建Apigee基础设施时,通过terraform_remote_state获取远程状态数据,在target_server子模块的apigee_target_server资源中使用for_each元参数时,触发以下错误:
- var.target_server_json will be known only after apply
The "for_each" map includes keys derived from resource attributes that cannot be determined until apply, and so Terraform cannot determine the full set of keys that will identify the instances of this resource.
相关代码片段
根模块核心代码
locals { artifacts_downloads_path = "${path.cwd}/artifacts" proxy_zip_file_name = "${var.proxy_name}.${var.proxy_version}.zip" local_proxy_artifact_directory = "${local.artifacts_downloads_path}/${var.proxy_name}" script_file_directory = "${path.cwd}/scripts" terraform_state_output = data.terraform_remote_state.terraform_state_values.outputs } module "target_server" { for_each = { for targetServer in var.list_of_target_servers_entries : targetServer => targetServer } source = "../target_server" apigee_env = var.apigee_env target_server_json = jsondecode(element(values(local.terraform_state_output), index(keys(local.terraform_state_output), each.value))) depends_on = [data.terraform_remote_state.terraform_state_values] } data "terraform_remote_state" "terraform_state_values" { backend = "gcs" config = { bucket = var.terraform_state_bucket_name prefix = var.terraform_state_bucket_path_prefix } }
原target_server模块代码(触发错误)
resource "apigee_target_server" "target_server" { for_each = { for index, entry in jsondecode(var.target_server_json) : index => entry } environment_name = var.apigee_env name = each.value.name host = strcontains(each.value.host, "https") ? split("https://", each.value.host)[1] : each.value.host port = each.value.port is_enabled = each.value.enabled ? true : false protocols = each.value.protocol != [] ? [each.value.protocol] : [] }
尝试的count重构代码
locals { items_count = length(jsondecode(var.target_server_json)) } resource "apigee_target_server" "target_server" { count = local.items_count environment_name = var.apigee_env name = var.target_server_json[count.index].name host = strcontains(var.target_server_json[count.index].host, "https") ? split("https://", var.target_server_json[count.index].host)[1] : var.target_server_json[count.index].host port = var.target_server_json[count.index].port is_enabled = var.target_server_json[count.index].enabled ? true : false protocols = var.target_server_json[count.index].protocol != [] ? [var.target_server_json[count.index].protocol] : [] }
有效解决方案
方案1:优化远程状态输出与变量传递(推荐)
问题根源是var.target_server_json来自远程状态的动态查找,Terraform在plan阶段无法确定其内容。调整如下:
- 修改远程状态生成模块:将目标服务器数据处理为明确的
map或list(object)结构输出,而非原始JSON字符串,确保plan阶段即可识别数据结构。 - 根模块直接引用结构化数据:避免使用
element+index的动态查找方式,直接通过键名引用远程状态输出:
# 根模块中修改target_server模块调用 module "target_server" { for_each = var.list_of_target_servers_entries source = "../target_server" apigee_env = var.apigee_env # 假设远程状态输出中直接有target_servers映射,键为targetServer名称 target_servers = local.terraform_state_output.target_servers[each.value] depends_on = [data.terraform_remote_state.terraform_state_values] }
- 子模块定义强类型变量:
# target_server子模块变量定义 variable "target_servers" { type = list(object({ name = string host = string port = number enabled = bool protocol = list(string) })) description = "结构化的目标服务器列表" } # 子模块资源使用for_each(基于稳定的name字段作为键) resource "apigee_target_server" "target_server" { for_each = { for ts in var.target_servers : ts.name => ts } environment_name = var.apigee_env name = each.value.name host = strcontains(each.value.host, "https") ? split("https://", each.value.host)[1] : each.value.host port = each.value.port is_enabled = each.value.enabled protocols = each.value.protocol != [] ? each.value.protocol : [] }
这种方式让Terraform在plan阶段就能确定for_each的键集合,避免"known only after apply"错误,同时保证资源变更的稳定性(键基于名称而非索引)。
方案2:使用terraform_data延迟解析(Terraform 1.4+)
如果无法修改远程状态结构,可利用Terraform 1.4+新增的terraform_data资源,在apply阶段完成JSON解析,再基于解析结果创建资源:
# target_server子模块中 terraform_data "parsed_target_servers" { input = jsondecode(var.target_server_json) } resource "apigee_target_server" "target_server" { for_each = { for ts in terraform_data.parsed_target_servers.output : ts.name => ts } environment_name = var.apigee_env name = each.value.name host = strcontains(each.value.host, "https") ? split("https://", each.value.host)[1] : each.value.host port = each.value.port is_enabled = each.value.enabled protocols = each.value.protocol != [] ? each.value.protocol : [] }
注意:此方式下plan阶段无法预览目标服务器资源,仅能在apply阶段创建,适合临时过渡场景。
方案3:优化count实现(仅临时过渡)
如果必须使用count,需避免在plan阶段依赖未知的JSON解析结果,可在根模块提前解析并传递结构化数据:
# 根模块中修改target_server调用 module "target_server" { for_each = var.list_of_target_servers_entries source = "../target_server" apigee_env = var.apigee_env # 根模块提前解析JSON为结构化数据 target_servers = jsondecode(element(values(local.terraform_state_output), index(keys(local.terraform_state_output), each.value))) depends_on = [data.terraform_remote_state.terraform_state_values] } # target_server子模块中 variable "target_servers" { type = list(object({ name = string host = string port = number enabled = bool protocol = list(string) })) } resource "apigee_target_server" "target_server" { count = length(var.target_servers) environment_name = var.apigee_env name = var.target_servers[count.index].name host = strcontains(var.target_servers[count.index].host, "https") ? split("https://", var.target_servers[count.index].host)[1] : var.target_servers[count.index].host port = var.target_servers[count.index].port is_enabled = var.target_servers[count.index].enabled protocols = var.target_servers[count.index].protocol != [] ? var.target_servers[count.index].protocol : [] }
注意:count依赖列表索引,若列表顺序变更会导致资源重建,稳定性不如for_each。
内容的提问来源于stack exchange,提问作者mukul
相关产品推荐
相关产品推荐

