【问题标题】:How to solve for_each + "Terraform cannot predict how many instances will be created" issue?如何解决 for_each +“Terraform 无法预测将创建多少个实例”问题?
【发布时间】:2022-01-05 16:53:53
【问题描述】:

我正在尝试用这个创建一个 GCP 项目:

module "project-factory" {
  source  = "terraform-google-modules/project-factory/google"
  version = "11.2.3"

  name              = var.project_name
  random_project_id = "true"
  org_id            = var.organization_id
  folder_id         = var.folder_id
  billing_account   = var.billing_account
  activate_apis = [
    "iam.googleapis.com",
    "run.googleapis.com"
  ]
}

之后,我正在尝试创建一个服务帐户,如下所示:

module "service_accounts" {
  source  = "terraform-google-modules/service-accounts/google"
  version = "4.0.3"

  project_id    = module.project-factory.project_id
  generate_keys = "true"
  names         = ["backend-runner"]
  project_roles = [
    "${module.project-factory.project_id}=>roles/cloudsql.client",
    "${module.project-factory.project_id}=>roles/pubsub.publisher"
  ]
}

说实话,我对 Terraform 还很陌生。我已经阅读了有关该主题的一些答案(thisthis),但我无法理解这将如何适用于此。

我收到错误:


│ Error: Invalid for_each argument
│
│   on .terraform/modules/pubsub-exporter-service-account/main.tf line 47, in resource "google_project_iam_member" "project-roles":
│   47:   for_each = local.project_roles_map_data
│     ├────────────────
│     │ local.project_roles_map_data will be known only after apply
│
│ The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot predict how many instances will be created. To work around this, use the
│ -target argument to first apply only the resources that the for_each depends on.

期待通过这次挑战了解更多关于 Terraform 的信息。

【问题讨论】:

  • 答案是use tags 来解决这个问题吗?
  • 你能分享正在使用for_each的资源吗?
  • 您在for_each 中使用的变量必须在编译时知道。它不能是动态的。

标签: terraform terraform-provider-gcp


【解决方案1】:

这里只有部分配置可见,我猜测了一下,但让我们看看。您提到您想在本练习中了解更多有关 Terraform 的信息,因此我将在此处详细介绍有关链的内容,以解释 为什么我推荐我'我会推荐,但如果你觉得这个额外的细节无趣,你可以跳到最后。

我们将从that first module's definition of its project_id output value开始:

output "project_id" {
  value = module.project-factory.project_id
}

module.project-factory这里指的是a nested module call,所以我们需要再深入一层in the nested module terraform-google-modules/project-factory/google//modules/core_project_factory

output "project_id" {
  value = module.project_services.project_id
  depends_on = [
    module.project_services,
    google_project.main,
    google_compute_shared_vpc_service_project.shared_vpc_attachment,
    google_compute_shared_vpc_host_project.shared_vpc_host,
  ]
}

另一个嵌套模块调用! ? 那个人像这样声明its project_id

output "project_id" {
  description = "The GCP project you want to enable APIs on"
  value       = element(concat([for v in google_project_service.project_services : v.project], [var.project_id]), 0)
}

呸! ? 最后是一个实际的资源。在这种情况下,此表达式似乎采用了 google_project_service 资源实例的 project 属性,或者如果在此模块实例中禁用了该资源,则可能从 var.project_id 中获取它。一起来看看the google_project_service.project_services definition

resource "google_project_service" "project_services" {
  for_each                   = local.services
  project                    = var.project_id
  service                    = each.value
  disable_on_destroy         = var.disable_services_on_destroy
  disable_dependent_services = var.disable_dependent_services
}

project 这里设置为var.project_id,所以看起来像either way 这个最里面的project_id 输出只是反映了project_id 输入变量的值,所以我们需要跳回上一级并查看 the module call 到此模块以查看设置的内容:

module "project_services" {
  source = "../project_services"

  project_id                  = google_project.main.project_id
  activate_apis               = local.activate_apis
  activate_api_identities     = var.activate_api_identities
  disable_services_on_destroy = var.disable_services_on_destroy
  disable_dependent_services  = var.disable_dependent_services
}

project_id 设置为google_project.mainproject_id 属性:

resource "google_project" "main" {
  name                = var.name
  project_id          = local.temp_project_id
  org_id              = local.project_org_id
  folder_id           = local.project_folder_id
  billing_account     = var.billing_account
  auto_create_network = var.auto_create_network

  labels = var.labels
}

project_id 这里设置为local.temp_project_id,在同一个文件中进一步声明:

  temp_project_id = var.random_project_id ? format(
    "%s-%s",
    local.base_project_id,
    random_id.random_project_id_suffix.hex,
  ) : local.base_project_id

此表达式包含对random_id.random_project_id_suffix.hex 的引用,而.hex 是来自random_idresult 属性,因此由于@ 987654359@ 资源类型已实现。 (它会在应用步骤中生成一个随机值并将其保存在状态中,以便在以后的运行中保持一致。)


这意味着(在所有这些间接之后)你的模块中的module.project-factory.project_id 不是在配置中静态定义的值,而是可能在应用步骤期间动态决定。这意味着它不适合用作资源实例键的一部分,因此不适合用作 for_each 映射中的键。

不幸的是,这里for_each 的使用隐藏在另一个模块terraform-google-modules/service-accounts/google 中,所以我们也需要看看那个模块,看看它是如何使用project_roles 输入变量的。先来看看the specific resource block the error message was talking about

resource "google_project_iam_member" "project-roles" {
  for_each = local.project_roles_map_data

  project = element(
    split(
      "=>",
      each.value.role
    ),
    0,
  )

  role = element(
    split(
      "=>",
      each.value.role
    ),
    1,
  )

  member = "serviceAccount:${google_service_account.service_accounts[each.value.name].email}"
}

这里发生了一些有点复杂的事情,但与我们在这里看到的最相关的是,这个资源配置正在根据local.project_roles_map_data 的内容创建多个实例。现在让我们看看local.project_roles_map_data

  project_roles_map_data = zipmap(
    [for pair in local.name_role_pairs : "${pair[0]}-${pair[1]}"],
    [for pair in local.name_role_pairs : {
      name = pair[0]
      role = pair[1]
    }]
  )

这里稍微复杂一点,这对我们正在寻找的东西并不重要;这里要考虑的主要事情是,这是构造一个映射,其键是从元素零和 local.name_role_pairs 的元素一构建的,它直接在上面声明,以及它引用的 local.names

  names                 = toset(var.names)
  name_role_pairs       = setproduct(local.names, toset(var.project_roles))

所以我们在这里了解到的是var.names 中的值和var.project_roles 中的值都有助于该资源上for_each 的键,这意味着这些变量值都不应该包含任何内容在应用步骤中动态决定。

但是,我们还了解到(以上)google_project_iam_member.project-rolesprojectrole 参数源自您在您的自己的模块调用。


让我们回到我们开始的地方,记住所有这些额外的信息:

module "service_accounts" {
  source  = "terraform-google-modules/service-accounts/google"
  version = "4.0.3"

  project_id    = module.project-factory.project_id
  generate_keys = "true"
  names         = ["backend-runner"]
  project_roles = [
    "${module.project-factory.project_id}=>roles/cloudsql.client",
    "${module.project-factory.project_id}=>roles/pubsub.publisher"
  ]
}

我们了解到namesproject_roles 都必须只包含配置中决定的静态值,因此使用module.project-factory.project_id 是不合适的,因为直到随机项目ID 才能知道它已在应用步骤中生成。

然而,我们知道这个模块期望project_roles中每个项目的前缀(=>之前的部分)是一个有效的项目ID,所以没有任何其他可以合理使用的值。

因此我们陷入了困境:第二个模块有一个相当尴尬的设计决策,它试图从相同的值,而这两种情况有相互冲突的要求。但这不是您创建的模块,因此您无法轻松修改它以解决该设计怪癖。

鉴于此,我看到了两种可能的前进方法,虽然都不理想,但都有一些注意事项是可行的:

  • 您可以采用提供的错误消息作为解决方法,要求 Terraform 首先单独计划和应用第一个模块中的资源,然后在项目 ID 已经确定并在后续运行中计划和应用其余部分记录状态:

    terraform apply -target=module.factory
    terraform apply
    

    虽然必须分两步进行初始创建很烦人,但它至少只对这个基础架构的初始创建 很重要。如果您稍后对其进行更新,则无需重复此两步过程,除非您以需要生成新项目 ID 的方式更改了配置。

  • 在完成上述操作时,我们看到根据第一个模块的 var.random_project_id(您在配置中将其设置为 "true"),这种生成和返回随机项目 ID 的方法是可选的。否则,project_id 输出将只是您给定 name 参数的副本,该参数似乎是通过引用根模块变量来静态定义的。

    除非您特别需要在您的项目 ID 上使用随机后缀,否则您可以不设置 random_project_id,从而将项目 ID 设置为与您的 var.project_name 相同的静态值,这应该然后是一个可接受的值,可用作for_each 键。

理想情况下,第二个模块将被设计为将它使用的值(例如键)与它用于引用真实远程对象的值分开,因此可以使用远程对象的随机后缀名称,但是本地对象的静态定义名称。如果这是您控制下的模块,那么我会建议进行类似的设计更改,但我认为该第三方模块当前不寻常的设计(将多个值打包到带有分隔符的单个字符串中)是一种折衷方案希望保持与模块早期迭代的向后兼容性。

【讨论】:

    猜你喜欢
    • 2021-07-29
    • 2020-06-28
    • 1970-01-01
    • 2020-12-16
    • 1970-01-01
    • 1970-01-01
    • 2019-12-16
    • 2021-11-17
    • 1970-01-01
    相关资源
    最近更新 更多