更新CloudFormation自定义资源为何会导致其被删除?
解决CloudFormation自定义资源更新时误触发删除逻辑的问题
这个坑我也踩过!本质原因是CloudFormation处理自定义资源更新的机制:当你更新自定义资源的属性且返回了新的PhysicalResourceId时,CloudFormation会先创建一个新的资源实例,然后在UPDATE_COMPLETE_CLEANUP_IN_PROGRESS阶段删除旧的实例——这时候发送的Delete事件和栈删除时的Delete事件几乎一模一样,唯一的区别可以通过事件里的字段和你自己的状态跟踪来区分。
下面是几种可靠的区分方法,按推荐程度排序:
方法1:尽量返回相同的PhysicalResourceId(优先推荐)
如果你的自定义资源支持原地更新(比如修改S3桶的标签、Route53记录的属性,不需要重建资源),在处理Update事件时直接返回原来的PhysicalResourceId即可。这样CloudFormation会认为是原地修改资源,不会触发后续的Delete清理事件,从根源上避免问题。
示例代码片段(Python):
def handler(event, context): if event['RequestType'] == 'Update': # 执行原地更新逻辑(比如修改资源属性) return { 'Status': 'SUCCESS', 'PhysicalResourceId': event['PhysicalResourceId'], # 返回原ID 'Data': {} }
方法2:通过OldResourceProperties字段判断
当Delete事件是更新阶段的旧资源清理时,CloudFormation会在事件中携带OldResourceProperties字段(存储旧的资源属性);而栈删除时的Delete事件没有这个字段。这是最直接的区分方式。
示例判断逻辑:
def handler(event, context): if event['RequestType'] == 'Delete': # 检查是否存在旧属性,判断是更新清理还是栈删除 if 'OldResourceProperties' in event: # 这是更新过程中删除旧资源,只清理旧资源的残留(比如临时文件、旧记录) print(f"Cleaning up old resource {event['PhysicalResourceId']} during update") # 这里写旧资源的清理逻辑,不要删除新资源 else: # 真实的栈删除,执行完整的资源销毁逻辑 print(f"Performing full delete for resource {event['PhysicalResourceId']}") # 这里写删除核心资源的逻辑 return {'Status': 'SUCCESS', 'PhysicalResourceId': event['PhysicalResourceId']}
方法3:用状态存储跟踪活跃资源ID
如果你的自定义资源必须生成新的PhysicalResourceId(比如创建新的AMI、新的S3桶),可以用DynamoDB存储每个栈+逻辑资源对应的活跃PhysicalResourceId:
- 处理Create/Update事件时,将当前有效的
PhysicalResourceId存入DynamoDB(键用StackId-LogicalResourceId) - 收到Delete事件时,检查当前的
PhysicalResourceId是否是活跃ID:- 如果是,说明是栈删除,执行完整删除逻辑
- 如果不是,说明是更新时的旧资源清理,只做残留清理
示例代码片段:
import boto3 dynamodb = boto3.resource('dynamodb') tracker_table = dynamodb.Table('CustomResourceTracker') def handler(event, context): stack_logical_key = f"{event['StackId']}-{event['LogicalResourceId']}" if event['RequestType'] == 'Create': new_physical_id = "your-generated-resource-id" # 记录活跃资源 tracker_table.put_item(Item={ 'StackLogicalKey': stack_logical_key, 'ActivePhysicalId': new_physical_id }) return {'Status': 'SUCCESS', 'PhysicalResourceId': new_physical_id} elif event['RequestType'] == 'Update': new_physical_id = "your-updated-resource-id" # 更新活跃资源记录 tracker_table.put_item(Item={ 'StackLogicalKey': stack_logical_key, 'ActivePhysicalId': new_physical_id }) return {'Status': 'SUCCESS', 'PhysicalResourceId': new_physical_id} elif event['RequestType'] == 'Delete': # 查询当前活跃资源ID response = tracker_table.get_item(Key={'StackLogicalKey': stack_logical_key}) active_id = response.get('Item', {}).get('ActivePhysicalId') if event['PhysicalResourceId'] == active_id: # 真实栈删除,执行完整清理 print(f"Deleting active resource {active_id}") # 清理核心资源 tracker_table.delete_item(Key={'StackLogicalKey': stack_logical_key}) else: # 更新时的旧资源清理,只做残留处理 print(f"Cleaning up outdated resource {event['PhysicalResourceId']}") return {'Status': 'SUCCESS', 'PhysicalResourceId': event['PhysicalResourceId']}
注意事项
- 给Lambda角色添加DynamoDB的读写权限,确保能正常访问跟踪表
- 处理Delete事件时,无论哪种情况都要返回
SUCCESS,避免CloudFormation栈卡住 - 如果用状态存储,要考虑栈删除时的异常情况(比如Lambda没机会删除DynamoDB记录),可以给表加TTL自动清理过期记录
内容的提问来源于stack exchange,提问作者Guss




