使用for_each调用含aws_s3_bucket_notification的模块时资源互换问题问询
我有一个Terraform模块,其中包含针对单个S3桶的aws_s3_bucket_notification资源。该模块通过外部的for_each循环调用,所有由for_each生成的通知资源对应同一个桶,仅filter_prefix和lambda_function_arn参数不同。
当前循环包含两个配置(“foo”和“bar”),生成两个模块实例,在Terraform状态中分别为module.my_module["foo"]和module.my_module["bar"],对应两个实际通知资源:
module.my_module["foo"].aws_s3_bucket_notification.my_notificationmodule.my_module["bar"].aws_s3_bucket_notification.my_notification
奇怪的是,每次执行terraform apply时,这两个aws_s3_bucket_notification资源会互换配置:每隔一次执行,“foo”对应的通知会使用“bar”的filter_prefix和Lambda函数ARN,反之亦然,其余时候模块名称与通知配置的关联是正确的。
从技术角度看,功能未受影响,但这会导致Terraform状态混乱,产生不必要的变更计划。
请问这是预期行为吗?或是潜在Bug?还是我的实现方式本身有误——即通过for_each循环调用模块,为同一桶创建多个不同lambda和filter_prefix的aws_s3_bucket_notification资源?
更新1
以下是当前状态(terraform state show)和待执行计划的伪匿名输出,这看起来并非预期行为:
--- terraform show state 'module.my_module["AAAA"].module.my_submodule.aws_s3_bucket_notification.my_notification' # module.my_module["AAAA"].module.my_submodule.aws_s3_bucket_notification.my_notification: resource "aws_s3_bucket_notification" "my_notification" { bucket = "my_bucket" eventbridge = false id = "my_bucket" lambda_function { events = [ "s3:ObjectCreated:*", ] filter_prefix = "some_prefix_AAAA" id = "tf-s3-lambda-...00000001" lambda_function_arn = "arn:aws:lambda:<region>:<account_id>:function:some_lambda_AAAA" } } --- terraform show state 'module.my_module["BBBB"].module.my_submodule.aws_s3_bucket_notification.my_notification' # module.my_module["BBBB"].module.my_submodule.aws_s3_bucket_notification.my_notification: resource "aws_s3_bucket_notification" "my_notification" { bucket = "my_bucket" eventbridge = false id = "my_bucket" lambda_function { events = [ "s3:ObjectCreated:*", ] filter_prefix = "some_prefix_BBBB" id = "tf-s3-lambda-...00000002" lambda_function_arn = "arn:aws:lambda:<region>:<account_id>:function:some_lambda_BBBB" } } --- terraform plan Terraform will perform the following actions: # module.my_module["AAAA"].module.my_submodule.aws_s3_bucket_notification.my_notification will be updated in-place ~ resource "aws_s3_bucket_notification" "my_notification" { id = "my_bucket" # (2 unchanged attributes hidden) ~ lambda_function { ~ filter_prefix = "some_prefix_BBBB" -> "some_prefix_AAAA" id = "tf-s3-lambda-...00000002" ~ lambda_function_arn = "arn:aws:lambda:<region>:<account_id>:function:some_lambda_BBBB" -> "arn:aws:lambda:<region>:<account_id>:function:some_lambda_AAAA" # (1 unchanged attribute hidden) } } Plan: 0 to add, 1 to change, 0 to destroy.
更新2
该行为的原因显然是每个S3桶仅能存在一个aws_s3_bucket_notification资源。
我现在需要解决的问题是:如何静态创建这个空的aws_s3_bucket_notification资源,同时从完全不同的模块/代码库动态创建对应的lambda_function条目。
核心问题本质
AWS S3桶的通知配置是全局唯一的——整个桶只能有一份完整的通知配置,而非多个独立的aws_s3_bucket_notification资源。Terraform的AWS Provider会将每个aws_s3_bucket_notification资源映射到S3桶的整个通知配置,所以当你创建多个指向同一桶的该资源时,它们会互相覆盖对方的配置,导致状态混乱和无意义的变更循环。
实现思路:集中管理+动态注入
要实现“静态创建基础资源,多模块动态添加lambda通知”,可以通过以下方案:
1. 基础模块:创建空的S3通知资源
在管理S3桶的核心模块中,定义一个aws_s3_bucket_notification资源,初始时没有任何lambda_function、topic等配置,同时暴露一个输出变量用于后续扩展:
# 核心桶模块 main.tf resource "aws_s3_bucket" "main" { bucket = "my_bucket" # 其他桶配置... } resource "aws_s3_bucket_notification" "main" { bucket = aws_s3_bucket.main.id # 初始为空,不配置任何通知规则 } output "bucket_id" { value = aws_s3_bucket.main.id }
2. 动态模块:跨代码库/模块注入通知规则
根据代码库的分布情况,选择对应的实现方式:
方式A:跨代码库场景(使用远程状态)
在核心桶模块所在代码库,通过远程状态读取其他模块输出的lambda规则列表,动态合并到通知资源中:
# 核心桶模块 main.tf data "terraform_remote_state" "lambda_notifications" { backend = "s3" # 替换为你使用的后端类型 config = { bucket = "your-terraform-state-bucket" key = "path/to/other-module/state-file" region = "us-east-1" # 替换为你的状态桶区域 } } resource "aws_s3_bucket_notification" "main" { bucket = aws_s3_bucket.main.id # 遍历远程状态中的lambda规则,生成配置块 dynamic "lambda_function" { for_each = data.terraform_remote_state.lambda_notifications.outputs.lambda_rules content { events = lambda_function.value.events filter_prefix = lambda_function.value.filter_prefix lambda_function_arn = lambda_function.value.arn } } }
在其他代码库的lambda模块中,输出标准化的规则列表:
# 其他代码库的lambda模块 outputs.tf output "lambda_rules" { value = [ { events = ["s3:ObjectCreated:*"] filter_prefix = "some_prefix_AAAA" arn = aws_lambda_function.aaa.arn }, { events = ["s3:ObjectCreated:*"] filter_prefix = "some_prefix_BBBB" arn = aws_lambda_function.bbb.arn } ] }
方式B:同代码库多模块场景(直接参数传递)
如果所有模块在同一代码库,直接通过模块输入参数传递lambda规则列表:
# 核心桶模块 variables.tf variable "lambda_notifications" { type = list(object({ events = list(string) filter_prefix = string lambda_function_arn = string })) default = [] } # 核心桶模块 main.tf resource "aws_s3_bucket_notification" "main" { bucket = aws_s3_bucket.main.id dynamic "lambda_function" { for_each = var.lambda_notifications content { events = lambda_function.value.events filter_prefix = lambda_function.value.filter_prefix lambda_function_arn = lambda_function.value.lambda_function_arn } } }
在根模块中调用核心桶模块时,传入其他lambda模块的规则:
# 根模块 main.tf module "s3_bucket" { source = "./modules/s3-bucket" # 其他桶配置参数... lambda_notifications = [ { events = ["s3:ObjectCreated:*"] filter_prefix = "some_prefix_AAAA" lambda_function_arn = module.lambda_aaa.function_arn }, { events = ["s3:ObjectCreated:*"] filter_prefix = "some_prefix_BBBB" lambda_function_arn = module.lambda_bbb.function_arn } ] } module "lambda_aaa" { source = "./modules/lambda" # lambda A的配置... } module "lambda_bbb" { source = "./modules/lambda" # lambda B的配置... }
关键注意事项
- 确保仅存在一个
aws_s3_bucket_notification资源指向目标S3桶,所有通知规则都集中在该资源中管理。 - 跨代码库场景下,需确保核心模块拥有读取其他模块远程状态的权限。
- 动态块的
for_each使用带有唯一标识的列表元素,避免因元素顺序变化触发不必要的变更。
内容的提问来源于stack exchange,提问作者Hendrik Wiese

