如何解决 Terraform 不可预测的实例创建问题?

how to fix terraform unpredict instance creation issue?

我在 运行 terraform 计划和应用时遇到以下错误

on main.tf line 517, in resource "aws_lb_target_group_attachment" "ecom-tga":
│  517:    for_each          = local.service_instance_map
│     ├────────────────
│     │ local.service_instance_map 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.

我的配置文件如下

     variable "instance_count" {
          type = string
          default = 3
        }
        variable "service-names" {
          type = list
          default = ["valid","jsc","test"]
          
        }
    locals {
      helper_map = {for idx, val in setproduct(var.service-names, range(var.instance_count)): 
                       idx => {service_name = val[0]}
                   }
    }
        resource "aws_instance" "ecom-validation-service" {
        
           for_each      = local.helper_map 
        
           ami           = data.aws_ami.ecom.id
           instance_type = "t3.micro"
           tags = {
             Name = "${each.value.service_name}-service"
           }
           vpc_security_group_ids = [data.aws_security_group.ecom-sg[each.value.service_name].id]
           subnet_id = data.aws_subnet.ecom-subnet[each.value.service_name].id
        }

data "aws_instances" "ecom-instances" {
  for_each = toset(var.service-names)
  instance_tags = {
    Name = "${each.value}-service"
  }
  instance_state_names = ["running", "stopped"]
  depends_on = [
  aws_instance.ecom-validation-service
  ]
}
        
    locals {
        service_instance_map = merge([for env, value in data.aws_instances.ecom-instances:
                          {
                            for id in value.ids:
                            "${env}-${id}" => {
                              "service-name" = env
                              "id" = id
                            }
                          }
                        ]...)
        }
        
        resource "aws_lb_target_group_attachment" "ecom-tga" {
           for_each          = local.service_instance_map
           target_group_arn  = aws_lb_target_group.ecom-nlb-tgp[each.value.service-name].arn
           port              = 80
           target_id         = each.value.id
           depends_on = [aws_lb_target_group.ecom-nlb-tgp]
        }

因为我将 count 作为 var 传递并且它的值为 3,我认为 terraform 会预测,因为它需要创建 9 instances.But 它似乎没有并且抛出错误无法预测。

我们是否必须通过为实例计数预测或本地 service_instance_map 提供一些默认值来绕过此问题?

尝试了 try 功能,但还是不行

Error: Invalid for_each argument
│ 
│   on main.tf line 527, in resource "aws_lb_target_group_attachment" "ecom-tga":
│  527:    for_each          = try(local.service_instance_map,[])
│     ├────────────────
│     │ local.service_instance_map 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.

我的要求发生了变化,现在我必须在可用的 3 个子网中创建 3 个实例 region.I 更改了局部变量,如下所示但是相同的预测问题

locals {
  merged_subnet_svc = try(flatten([
    for service in var.service-names : [
      for subnet in aws_subnet.ecom-private.*.id : {
        service = service
        subnet  = subnet
      }
    ]
  ]), {})
variable "azs" {
  type    = list(any)
  default = ["ap-south-1a", "ap-south-1b", "ap-south-1c"]
}

variable "private-subnets" {
  type    = list(any)
  default = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
}

resource "aws_instance" "ecom-instances" {
  for_each = {
    for svc in local.merged_subnet_svc : "${svc.service}-${svc.subnet}" => svc
  }

  ami           = data.aws_ami.ecom.id
  instance_type = "t3.micro"
  tags = {
    Name = "ecom-${each.value.service}-service"
  }

  vpc_security_group_ids = [aws_security_group.ecom-sg[each.value.service].id]
  subnet_id              = each.value.subnet
}

}

在您的配置中,您已声明 data "aws_instances" "ecom-instances" 依赖于 aws_instance.ecom-validation-service。由于该其他对象在您的第一个 运行 上尚不存在,因此 Terraform 必须等到应用步骤才能读取 data.aws_instances.ecom-instances,否则它将无法遵守您声明的依赖项,因为 aws_instance.ecom-validation-service 还不存在。

为避免您在此处看到的错误消息,您需要确保 for_each 仅引用 Terraform 在 实际创建任何对象之前 知道的值。因为 EC2 仅在实例创建后分配实例 ID,所以使用 EC2 实例 ID 作为 for_each 实例键的一部分是不正确的。

此外,这里不需要 data "aws_instances" 块来检索实例信息,因为作为 resource "aws_instance" "ecom-validation-service" 块的结果,您已经拥有相关的实例信息。

综上所述,让我们从您的输入变量开始并重新构建,同时确保我们只根据我们在规划期间知道的值构建实例键。您拥有的变量基本保持不变;我只是稍微调整了类型约束以匹配我们如何使用每个约束:

variable "instance_count" {
  type    = string
  default = 3
}
variable "service_names" {
  type    = set(string)
  default = ["valid", "jsc", "test"]
}

我从您示例的其余部分了解到您打算为 var.service_names 的每个不同元素创建 var.instance_count 个实例。您的 setproduct 生成所有这些组合也很好,但我将对其进行调整以分配包含服务名称的实例唯一键:

locals {
  instance_configs = tomap({
    for pair in setproduct(var.service_names, range(var.instance_count)) :
    "${pair[0]}${pair[1]}" => {
      service_name = pair[0]
    }
  })
}

这将产生如下数据结构:

{
  valid0 = { service_name = "valid" }
  valid1 = { service_name = "valid" }
  valid2 = { service_name = "valid" }
  jsc0   = { service_name = "jsc" }
  jsc1   = { service_name = "jsc" }
  jsc2   = { service_name = "jsc" }
  test0  = { service_name = "test" }
  test1  = { service_name = "test" }
  test2  = { service_name = "test" }
}

这与 for_each 期望的形状匹配,因此我们可以直接使用它来声明九个 aws_instance 个实例:

resource "aws_instance" "ecom-validation-service" {
  for_each = local.instance_configs
        
  instance_type = "t3.micro"
  ami           = data.aws_ami.ecom.id
  subnet_id     = data.aws_subnet.ecom-subnet[each.value.service_name].id
  vpc_security_group_ids = [
    data.aws_security_group.ecom-sg[each.value.service_name].id,
  ]
  tags = {
    Name    = "${each.value.service_name}-service"
    Service = each.value_service_name
  }
}

到目前为止,这与您分享的内容基本相同。但这是我要朝着完全不同的方向前进的一点:现在我不再尝试使用单独的数据资源回读声明的实例,而是直接从 aws_instance.ecom-validation-service 资源。 Terraform 配置通常最好 管理特定对象 读取它,而不是同时读取,因为这样必要的依赖性排序会自动显示为参考文献。

请注意,我在每个实例上添加了一个额外的标签 Service,以提供一种更方便的方法来取回服务名称。如果你不能这样做,那么你可以通过从 Name 标签中删除 -service 后缀来获得相同的信息,但我更喜欢尽可能直接。

您当时的目标似乎是每个实例有一个 aws_lb_target_group_attachment 个实例,每个实例都根据服务名称连接到适当的目标组。因为 aws_instance 资源设置了 for_each,所以其他地方的表达式中的 aws_instance.ecom-validation-service 是一个对象映射,其中的键与 var.instance_configs 中的键相同。这意味着该值 for_each 的要求兼容,因此我们可以直接使用它来声明目标组附件:

resource "aws_lb_target_group_attachment" "ecom-tga" {
  for_each = aws_instance.ecom-validation-service

  target_group_arn  = aws_lb_target_group.ecom-nlb-tgp[each.value.tags.Service].arn
  port              = 80
  target_id         = each.value.id
}

我依靠之前的额外 Service 标记轻松确定每个实例属于哪个服务,以便查找适当的目标组 ARN。 each.value.id 在这里工作是因为 each.value 始终是一个 aws_instance 对象,它导出 id 属性。

结果是两组实例,每个实例都具有与 local.instance_configs 中的键匹配的键:

  • aws_instance.ecom-validation-service["valid0"]
  • aws_instance.ecom-validation-service["valid1"]
  • aws_instance.ecom-validation-service["valid2"]
  • aws_instance.ecom-validation-service["jsc0"]
  • aws_instance.ecom-validation-service["jsc1"]
  • aws_instance.ecom-validation-service["jsc2"]
  • ...
  • aws_lb_target_group_attachment.ecom-tga["valid0"]
  • aws_lb_target_group_attachment.ecom-tga["valid1"]
  • aws_lb_target_group_attachment.ecom-tga["valid2"]
  • aws_lb_target_group_attachment.ecom-tga["jsc0"]
  • aws_lb_target_group_attachment.ecom-tga["jsc1"]
  • aws_lb_target_group_attachment.ecom-tga["jsc2"]
  • ...

请注意,所有这些键仅包含在配置中直接指定的信息,而不包含远程系统决定的任何信息。这意味着我们避免了“无效的 for_each 参数”错误,即使每个实例仍然有一个适当的唯一键。如果您稍后要向 var.service_names 添加新元素或增加 var.instance_count,那么 Terraform 还将从这些实例键的形状中看到它应该只添加每个资源的新实例,而不是 renaming/renumbering 任何现有实例。