把任务交给一个能自己写Terraform、写Go、写Lambda处理函数、连S3都接好的AI开发环境,会生出一种特别的自信。你描述一个功能,它把代码写完、部署上线,主流程跑通。你心想:没问题了,我可以去干点别的了。 于是某个下午,我把一个任务丢给Kiro,按下开始,让它自己跑。正好这时后院也有件事等着我,我就把Kiro留在那儿,自己去忙了。 几个小时后,手机响了。是AWS发来的消息:他们检测到我的账户里,一个Lambda函数和一个S3存储桶之间出现了递归循环,为了止血,已经帮我停掉了这个函数的调用。 **这个项目是干什么的** 我一直在AWS上搭一个很小的无服务器食谱服务,给家里人用的私人食谱库。你可以把一道菜以照片、一段文字或者一个链接的形式丢进去,系统会把它提取出来、清理干净、翻译成芬兰语,再拆成原料和步骤存好。全部用Terraform,全部无服务器,Lambda上跑Go,AI部分交给Bedrock,大部分代码由Kiro来敲。 这类项目配一个AI搭子来做确实有意思,因为活儿基本都是管道工程:DynamoDB单表设计、S3预签名上传、API Gateway路由、一条Step Functions流水线。没有一样是难的,但每一样都琐碎。正是那种你乐意交出去的活。 **那天下午要做的功能** 那个下午的任务跟食谱本身无关,是关于额外的照片:做饭时随手拍的,或者烤好的成品摆在台面上的。想法是家里任何人都能把自己的照片挂到某道菜下面——面团刚搅到一半的样子、蛋糕出炉的样子、装盘后的样子。每道菜配一个小小的照片墙。 这里唯一算得上正经工作的是尺寸问题。现在手机直接拍出来的照片有好几兆,你既不想把这种图塞进缩略图网格,也不想在点开放大的灯箱里加载完整原图。所以每张上传的照片,系统需要生成两个衍生版本: - 一张320像素的缩略图,给照片墙网格用。 - 一张约1024像素的“展示”版,给点击放大的视图用。 有一点值得说清楚:我给Kiro的指令只是“从每张上传的图片生成这两个版本”,仅此而已。我完全没提这些衍生图该存在哪里,没要求它们写回原图旁边,没要求写进同一个桶,也没指定任何位置。那是Kiro自己的实现选择,看起来完全合理,而正是这个选择埋下了陷阱。 Kiro搭出来的是这样:用户照片上传到S3,一个S3的ObjectCreated通知触发Lambda,Lambda读取上传的照片,把它缩放成缩略图和展示版,再写回同一个桶里,好让它们随时能被取用。 Kiro把它实现出来,接好Terraform,部署到我的账户上。然后为了验证主流程,它往服务里传了一张测试图。差不多就在这时候,几分钟后,我的笔记本进入了睡眠模式,Kiro也跟着安静了。改动已经生效在AWS上,一张真实的图片刚落进桶里,而这条刚部署好的照片流水线,此刻正完全靠自己跑着。 **那个我没闻出来的味道** 这就是那个bug,属于在脑子里想的时候看起来完全没问题的那种。 Lambda是被“桶里出现了新对象”触发的。它生成衍生图,写回同一个桶。这些写入就是桶里的新对象,于是触发通知,于是调用Lambda,于是生成衍生图写回桶里,于是触发通知。 上传一张照片,你处理的不是一张图,而是永远处理不完的图。每一轮写入两个新对象,这两个对象各自触发一次调用,每次调用又写出两个。这不是慢慢渗漏,是指数级的。 我的账户没被烧穿,唯一的原因是AWS对这类错误有安全网:它会检测Lambda和S3之间的递归调用循环,然后把函数切断。这就是我醒来看到的那封邮件。 说清楚一点,这是S3上一个人尽皆知的坑,已经被写了好多年。“别从一个桶触发Lambda、又写回同一个桶而不加防护”,这几乎算一条戒律。可我们还是踩了,不是因为我要求这么做,而是因为那个显而易见的实现读起来太无辜了:生成一张缩略图,存到原图旁边。当然要存到原图旁边,不然还能存哪儿?那个没说出口的“当然”,就是全部问题所在。我只要求了那两个版本,而存放它们最自然的位置,悄悄把循环闭上了。 **怎么修** 要打断这个循环,有几种实在的做法: - 分开的桶。原图放一个桶,衍生图放另一个桶。触发用的那个桶永远不接收Lambda自己的写入。 - 前缀过滤。所有东西都放一个桶里,但把S3通知限定到Lambda从不写入的前缀上。 - 在处理器里做后缀过滤。让通知照常触发,但让处理器认出自己产出的东西,立刻退出。 我最后同时用了其中两种,因为在引发了一次失控循环之后,你就不再信任单点防护了。 第一,命名约定加限定范围的触发器。上传的照片用 `-orig` 后缀存储(`-orig.jpg`);衍生图拿到的是 `-thumb.jpg` 和 `-display.jpg`。S3通知被过滤成只在 `-orig` 后缀上触发。所以Lambda自己的产出——永远不会以 `-orig` 结尾——根本不匹配触发器。反馈的那条边,在事件源头就被切断了。 第二,在处理器本身里加一道防御性的跳过,以防未来某个我把它配得更宽: // Loop guard: only act on "-orig" originals. Anything
![]()
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.