You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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
}

尝试过的方案及缺陷

  1. 禁用该资源的回滚:无法覆盖所有场景,部分情况下仍需要回滚能力,不可行。
  2. 通过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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.13 23:25:34