AWS自定义资源回滚陷入两难困境,出现ROLLBACK_FAILED
AWS CloudFormation自定义资源回滚Catch22问题及优化方案
问题背景
我维护一个AWS CloudFormation自定义资源,核心逻辑如下:
- 创建请求:在RDS中创建指定表
- 删除请求:删除RDS中指定表
- 更新请求:对比新旧资源属性,若检测到列值变更则返回错误(暂不支持列类型/属性变更)
但在栈回滚(ROLLBACK)场景下,该资源会陷入两难困境,导致ROLLBACK_FAILED状态,具体场景:
已解决场景
创建请求失败(比如SQL语法错误)时,回滚阶段会触发删除请求,但此时表并未创建成功,执行删除操作会报错,导致回滚失败。
未解决场景
更新请求因列值变更被拦截报错后,回滚阶段会触发新的更新请求——此时event.ResourceProperties与event.OldResourceProperties会交换,但原有的列值变更检查逻辑仍会触发错误,导致回滚失败。
简化代码示例
package main import ( "context" "encoding/json" "fmt" "strings" "github.com/aws/aws-lambda-go/cfn" "github.com/aws/aws-lambda-go/lambda" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/rdsdataservice" "github.com/google/uuid" ) func main() { lambda.Start(cfn.LambdaWrap(handler)) } type properties struct { TableName string Columns []struct { Name string Value string } } func handler(ctx context.Context, event cfn.Event) (physicalResourceID string, data map[string]interface{}, err error) { prid := event.PhysicalResourceID if event.RequestType == cfn.RequestCreate { prid = strings.ReplaceAll(uuid.New().String(), "-", "") } var props properties b, _ := json.Marshal(event.ResourceProperties) json.Unmarshal(b, &props) rds := rdsdataservice.New(nil) if event.RequestType == cfn.RequestCreate { _, err = rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("CREATE TABLE %s", props.TableName)), }) if err != nil { return "", nil, err } } if event.RequestType == cfn.RequestDelete { // 注:原代码此处误写为CREATE TABLE,修正为DROP TABLE _, err = rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("DROP TABLE %s", props.TableName)), }) if err != nil { return "", nil, err } } if event.RequestType == cfn.RequestUpdate { var oldProps properties b, _ := json.Marshal(event.OldResourceProperties) json.Unmarshal(b, &oldProps) columns := map[string]string{} for _, column := range props.Columns { columns[column.Name] = column.Value } for _, column := range oldProps.Columns { if val, ok := columns[column.Name]; ok { if val != column.Value { return "", nil, fmt.Errorf("cannot change column type") } } } // 此处可添加新增/删除列的逻辑 } return prid, nil, nil }
尝试过的方案及缺陷
- 禁用该资源的回滚:无法覆盖所有场景,部分情况下仍需要回滚能力,不可行。
- 通过
DescribeStackEvents判断状态:调用CloudFormation API获取栈事件来推断上次操作结果,但当栈事件量较大时,会出现分页查询繁琐、性能下降的问题,可靠性不足。
优化解决方案
方案1:基于实际资源状态的幂等处理
利用RDS的元数据查询,判断实际资源的存在状态,实现操作的幂等性,同时区分正常更新与回滚更新:
处理删除请求的幂等性
在执行删除操作前,先检查表是否存在,不存在则直接返回成功:
if event.RequestType == cfn.RequestDelete { // 查询目标表是否存在 existsResp, err := rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = '%s'", props.TableName)), }) if err != nil { return prid, nil, err } // 无返回记录说明表不存在,直接返回成功 if len(existsResp.Records) == 0 { return prid, nil, nil } // 表存在则执行删除 _, err = rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("DROP TABLE %s", props.TableName)), }) if err != nil { return prid, nil, err } }
处理更新回滚场景
回滚阶段的更新请求是要恢复到之前的资源状态,此时通过查询RDS实际表结构,判断是否是回滚操作:
if event.RequestType == cfn.RequestUpdate { var oldProps properties b, _ := json.Marshal(event.OldResourceProperties) json.Unmarshal(b, &oldProps) // 查询当前实际的表列结构 currentColumns := make(map[string]string) colResp, err := rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("SELECT COLUMN_NAME, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME = '%s'", props.TableName)), }) if err != nil { return prid, nil, err } // 解析查询结果到currentColumns(需根据RDS Data Service返回格式处理,此处省略具体解析逻辑) // 判断当前实际结构是否与旧属性一致,一致则为回滚场景 isRollback := true for _, col := range oldProps.Columns { if currentColumns[col.Name] != col.Value { isRollback = false break } } // 仅非回滚场景执行列变更检查 if !isRollback { columns := map[string]string{} for _, column := range props.Columns { columns[column.Name] = column.Value } for _, column := range oldProps.Columns { if val, ok := columns[column.Name]; ok { if val != column.Value { return "", nil, fmt.Errorf("cannot change column type") } } } } // 回滚场景直接通过,执行后续逻辑 // 此处可添加新增/删除列的逻辑 }
方案2:利用自定义资源Data字段记录状态
在创建或更新成功后,将当前生效的列结构存入自定义资源返回的Data字段。后续处理请求时,通过对比Data中的状态与请求属性,判断是否为回滚操作:
// 创建成功后记录当前列结构到Data if event.RequestType == cfn.RequestCreate { _, err = rds.ExecuteStatement(&rdsdataservice.ExecuteStatementInput{ Sql: aws.String(fmt.Sprintf("CREATE TABLE %s", props.TableName)), }) if err != nil { return "", nil, err } // 返回当前列结构到Data data = map[string]interface{}{ "Columns": props.Columns, } } // 更新请求中判断是否为回滚 if event.RequestType == cfn.RequestUpdate { var oldProps properties b, _ := json.Marshal(event.OldResourceProperties) json.Unmarshal(b, &oldProps) // 从旧资源的Data中获取之前生效的列结构 prevColumns := []struct{Name, Value string}{} if prevColRaw, ok := event.OldResourceData["Columns"]; ok { // 将prevColRaw解析为prevColumns结构 prevColBytes, _ := json.Marshal(prevColRaw) json.Unmarshal(prevColBytes, &prevColumns) } // 判断当前请求的新属性是否与之前生效的属性一致(回滚场景) isRollback := true currentColMap := make(map[string]string) for _, col := range props.Columns { currentColMap[col.Name] = col.Value } for _, col := range prevColumns { if currentColMap[col.Name] != col.Value { isRollback = false break } } // 非回滚场景才执行列变更检查 if !isRollback { // 原有列变更检查逻辑 columns := map[string]string{} for _, column := range props.Columns { columns[column.Name] = column.Value } for _, column := range oldProps.Columns { if val, ok := columns[column.Name]; ok { if val != column.Value { return "", nil, fmt.Errorf("cannot change column type") } } } } // 回滚场景直接通过,执行后续逻辑 // 此处可添加新增/删除列的逻辑 data = map[string]interface{}{ "Columns": props.Columns, } }
内容的提问来源于stack exchange,提问作者JanJetze
相关产品推荐
相关产品推荐

