更新自定义资源会导致它们被删除?
Updating custom resources causes them to be deleted?
在使用 CloudFormation 模板时,我发现 "Custom Resource" 功能及其 Lambda 支持函数实现,对于处理 CloudFormation 无法很好支持的各种任务非常有用。
通常,我使用自定义资源在堆栈创建期间设置内容(例如查找 AMI 名称)或在删除期间清理内容(例如从 S3 或 Route53 中删除会阻止删除的对象)- 这非常有效.
但是当我尝试实际使用 "custom resource" 来管理实际的自定义资源时,必须在堆栈创建期间创建,在堆栈删除期间删除,并且 - 这就是问题所在 - 有时会更新在堆栈更新期间使用新值时,CloudFormation 集成行为异常并导致自定义资源失败。
问题似乎是,在其中一个自定义资源属性发生更改的堆栈更新期间,在堆栈的 UPDATE_IN_PROGRESS
阶段,CloudFormation 将更新事件发送到支持 Lambda 函数,并设置了所有值正确并发送旧值的副本。但在更新完成后,CloudFormation 启动 UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
阶段并向后备 Lambda 函数发送删除事件(RequestType
设置为 Delete
)。
发生这种情况时,后备 lambda 函数假定正在删除堆栈并删除自定义资源。结果是更新后自定义资源消失了。
我查看了日志中的请求数据,"cleanup delete" 看起来与真实的 "delete" 事件相同:
清理删除:
{
RequestType: 'Delete',
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
ResponseURL: 'https://cloudformation-custom-resource-response-useast2.s3.us-east-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-2%3A1234567890%3Astack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1%7Cresnmae%7C15521ba8-1a3c-4594-9ea9-18513efb6e8d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20180511T140259Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=AKISOMEAWSKEYID%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Signature=3abc68e1f8df46a711a2f6084debaf2a16bd0acf7f58837b9d02c805975df91b',
StackId: 'arn:aws:cloudformation:us-east-2:1234567890:stack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1',
RequestId: '15521ba8-1a3c-4594-9ea9-18513efb6e8d',
LogicalResourceId: 'resname',
PhysicalResourceId: '2018/05/11/[$LATEST]28bad2681fb84c0bbf80990e1decbd97',
ResourceType: 'Custom::Resource',
ResourceProperties: {
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
VpcId: 'vpc-35512e5d',
SomeValue: '4'
}
}
真正删除:
{
RequestType: 'Delete',
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
ResponseURL: 'https://cloudformation-custom-resource-response-useast2.s3.us-east-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-2%3A1234567890%3Astack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1%7Cresname%7C6166ff92-009d-47ac-ac2f-c5be2c1a7ab2?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20180524T154453Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7200&X-Amz-Credential=AKISOMEAWSKEYID%2F20180524%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Signature=29ca1d0dbdbe9246f7f82c1782726653b2aac8cd997714479ab5a080bab03cac',
StackId: 'arn:aws:cloudformation:us-east-2:123456780:stack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1',
RequestId: '6166ff92-009d-47ac-ac2f-c5be2c1a7ab2',
LogicalResourceId: 'resname',
PhysicalResourceId: '2018/05/11/[$LATEST]c9494122976b4ef3a4102628fafbd1ec',
ResourceType: 'Custom::Resource',
ResourceProperties: {
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
VpcId: 'vpc-35512e5d',
SomeValue: '0'
}
}
我能看到的唯一有趣的请求字段是物理资源 ID 不同,但我不知道将其关联到什么,以检测它是否是真正的删除。
问题似乎是 sendResponse()
函数的示例实现,该函数用于将自定义资源完成事件发送回 CloudFormation。该方法负责设置自定义资源的物理资源ID。据我了解,此值表示由支持 CloudFormation 自定义资源的 Lambda 函数管理的“外部资源”的全局唯一标识符。
如 CloudFormation's "Lambda-backed Custom Resource" sample code, as well as in the cfn-response
NPM module's send()
and the CloudFormation's built-in cfn-response
module 中所示,此方法具有计算物理资源 ID 的默认行为,如果未作为第 5 个参数提供,则它使用正在处理的 CloudWatch Logs 日志流记录正在处理的请求:
var responseBody = JSON.stringify({
...
PhysicalResourceId: context.logStreamName,
...
})
由于 CloudFormation(或 AWS Lambda 运行时?)偶尔会将日志流更改为新日志流,因此 sendResponse()
生成的物理资源 ID 会不时发生意外变化,并混淆 CloudFormation。
据我了解,CloudFormation 托管实体有时需要在更新期间更换(一个很好的例子是 RDS::DBInstance
,几乎任何更改都需要更换)。 CloudFormation 策略是,如果资源需要替换,则在“更新阶段”创建新资源,在“清理阶段”删除旧资源。
所以使用默认的sendResponse()
物理资源ID计算,过程是这样的:
- 堆栈已创建。
- 创建了一个新的日志流来处理自定义资源日志记录。
- 调用后备 Lambda 函数来创建资源,默认行为将其资源 ID 设置为日志流 ID。
- 一段时间过去了
- 使用自定义资源的新参数更新堆栈。
- 使用新 ID 创建新的日志流来处理自定义资源日志记录。
- 调用后备 Lambda 函数来更新资源,默认行为将新的资源 ID 设置为新的日志流 ID。
- CloudFormation 知道创建了一个新资源来替换旧资源,并且根据策略它应该在“清理阶段”删除旧资源。
- CloudFormation 到达“清理阶段”并发送带有旧物理资源 ID 的删除请求。
至少在我从不“替换外部资源”的情况下,解决方案是为托管资源制作一个唯一标识符,将其作为发送响应例程的第 5 个参数提供,然后坚持使用- 在更新响应中继续发送更新请求中收到的相同物理资源 ID。 CloudFormation 将永远不会在“清理阶段”发送删除请求。
我的实现(在 JavaScript 中)看起来像这样:
var resID = event.PhysicalResourceId || uuid();
...
sendResponse(event, context, status, resData, resID);
另一种选择 - 这可能只有在您确实需要替换外部资源并希望遵守在清理期间删除旧资源的 CloudFormation 模型时才有意义 - 是使用实际的外部资源 ID 作为物理资源 ID,并在收到删除请求时 - 使用提供的物理资源 ID 删除旧的外部资源。这可能是 CloudFormation 设计者首先想到的,但他们的默认示例实现引起了很多混乱——可能是因为示例实现不管理真实资源并且没有更新功能。 CloudFormation 中也有零文档来解释设计和推理。
了解自定义资源生命周期非常重要,以防止您的数据被删除。
A very interesting and important thing to know is that CloudFormation
compares the physical resource id you returned by your Lambda function
to the one you returned previously. If the IDs are different,
CloudFormation assumes the resource has been replaced with a new
resource. Then something interesting happens.
When the resource update logic completes successfully, a Delete
request is sent with the old physical resource id. If the stack update
fails and a rollback occurs, the new physical resource id is sent in
the Delete event.
您可以阅读更多here关于自定义资源生命周期和其他最佳实践的内容
在使用 CloudFormation 模板时,我发现 "Custom Resource" 功能及其 Lambda 支持函数实现,对于处理 CloudFormation 无法很好支持的各种任务非常有用。
通常,我使用自定义资源在堆栈创建期间设置内容(例如查找 AMI 名称)或在删除期间清理内容(例如从 S3 或 Route53 中删除会阻止删除的对象)- 这非常有效.
但是当我尝试实际使用 "custom resource" 来管理实际的自定义资源时,必须在堆栈创建期间创建,在堆栈删除期间删除,并且 - 这就是问题所在 - 有时会更新在堆栈更新期间使用新值时,CloudFormation 集成行为异常并导致自定义资源失败。
问题似乎是,在其中一个自定义资源属性发生更改的堆栈更新期间,在堆栈的 UPDATE_IN_PROGRESS
阶段,CloudFormation 将更新事件发送到支持 Lambda 函数,并设置了所有值正确并发送旧值的副本。但在更新完成后,CloudFormation 启动 UPDATE_COMPLETE_CLEANUP_IN_PROGRESS
阶段并向后备 Lambda 函数发送删除事件(RequestType
设置为 Delete
)。
发生这种情况时,后备 lambda 函数假定正在删除堆栈并删除自定义资源。结果是更新后自定义资源消失了。
我查看了日志中的请求数据,"cleanup delete" 看起来与真实的 "delete" 事件相同:
清理删除:
{
RequestType: 'Delete',
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
ResponseURL: 'https://cloudformation-custom-resource-response-useast2.s3.us-east-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-2%3A1234567890%3Astack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1%7Cresnmae%7C15521ba8-1a3c-4594-9ea9-18513efb6e8d?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20180511T140259Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7199&X-Amz-Credential=AKISOMEAWSKEYID%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Signature=3abc68e1f8df46a711a2f6084debaf2a16bd0acf7f58837b9d02c805975df91b',
StackId: 'arn:aws:cloudformation:us-east-2:1234567890:stack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1',
RequestId: '15521ba8-1a3c-4594-9ea9-18513efb6e8d',
LogicalResourceId: 'resname',
PhysicalResourceId: '2018/05/11/[$LATEST]28bad2681fb84c0bbf80990e1decbd97',
ResourceType: 'Custom::Resource',
ResourceProperties: {
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
VpcId: 'vpc-35512e5d',
SomeValue: '4'
}
}
真正删除:
{
RequestType: 'Delete',
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
ResponseURL: 'https://cloudformation-custom-resource-response-useast2.s3.us-east-2.amazonaws.com/arn%3Aaws%3Acloudformation%3Aus-east-2%3A1234567890%3Astack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1%7Cresname%7C6166ff92-009d-47ac-ac2f-c5be2c1a7ab2?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20180524T154453Z&X-Amz-SignedHeaders=host&X-Amz-Expires=7200&X-Amz-Credential=AKISOMEAWSKEYID%2F20180524%2Fus-east-2%2Fs3%2Faws4_request&X-Amz-Signature=29ca1d0dbdbe9246f7f82c1782726653b2aac8cd997714479ab5a080bab03cac',
StackId: 'arn:aws:cloudformation:us-east-2:123456780:stack/stackname/3cc80cf0-5415-11e8-b6dc-503f3157b0d1',
RequestId: '6166ff92-009d-47ac-ac2f-c5be2c1a7ab2',
LogicalResourceId: 'resname',
PhysicalResourceId: '2018/05/11/[$LATEST]c9494122976b4ef3a4102628fafbd1ec',
ResourceType: 'Custom::Resource',
ResourceProperties: {
ServiceToken: 'arn:aws:lambda:us-east-2:1234567890:function:stackname-resname-J0LWT56QSPIA',
VpcId: 'vpc-35512e5d',
SomeValue: '0'
}
}
我能看到的唯一有趣的请求字段是物理资源 ID 不同,但我不知道将其关联到什么,以检测它是否是真正的删除。
问题似乎是 sendResponse()
函数的示例实现,该函数用于将自定义资源完成事件发送回 CloudFormation。该方法负责设置自定义资源的物理资源ID。据我了解,此值表示由支持 CloudFormation 自定义资源的 Lambda 函数管理的“外部资源”的全局唯一标识符。
如 CloudFormation's "Lambda-backed Custom Resource" sample code, as well as in the cfn-response
NPM module's send()
and the CloudFormation's built-in cfn-response
module 中所示,此方法具有计算物理资源 ID 的默认行为,如果未作为第 5 个参数提供,则它使用正在处理的 CloudWatch Logs 日志流记录正在处理的请求:
var responseBody = JSON.stringify({
...
PhysicalResourceId: context.logStreamName,
...
})
由于 CloudFormation(或 AWS Lambda 运行时?)偶尔会将日志流更改为新日志流,因此 sendResponse()
生成的物理资源 ID 会不时发生意外变化,并混淆 CloudFormation。
据我了解,CloudFormation 托管实体有时需要在更新期间更换(一个很好的例子是 RDS::DBInstance
,几乎任何更改都需要更换)。 CloudFormation 策略是,如果资源需要替换,则在“更新阶段”创建新资源,在“清理阶段”删除旧资源。
所以使用默认的sendResponse()
物理资源ID计算,过程是这样的:
- 堆栈已创建。
- 创建了一个新的日志流来处理自定义资源日志记录。
- 调用后备 Lambda 函数来创建资源,默认行为将其资源 ID 设置为日志流 ID。
- 一段时间过去了
- 使用自定义资源的新参数更新堆栈。
- 使用新 ID 创建新的日志流来处理自定义资源日志记录。
- 调用后备 Lambda 函数来更新资源,默认行为将新的资源 ID 设置为新的日志流 ID。
- CloudFormation 知道创建了一个新资源来替换旧资源,并且根据策略它应该在“清理阶段”删除旧资源。
- CloudFormation 到达“清理阶段”并发送带有旧物理资源 ID 的删除请求。
至少在我从不“替换外部资源”的情况下,解决方案是为托管资源制作一个唯一标识符,将其作为发送响应例程的第 5 个参数提供,然后坚持使用- 在更新响应中继续发送更新请求中收到的相同物理资源 ID。 CloudFormation 将永远不会在“清理阶段”发送删除请求。
我的实现(在 JavaScript 中)看起来像这样:
var resID = event.PhysicalResourceId || uuid();
...
sendResponse(event, context, status, resData, resID);
另一种选择 - 这可能只有在您确实需要替换外部资源并希望遵守在清理期间删除旧资源的 CloudFormation 模型时才有意义 - 是使用实际的外部资源 ID 作为物理资源 ID,并在收到删除请求时 - 使用提供的物理资源 ID 删除旧的外部资源。这可能是 CloudFormation 设计者首先想到的,但他们的默认示例实现引起了很多混乱——可能是因为示例实现不管理真实资源并且没有更新功能。 CloudFormation 中也有零文档来解释设计和推理。
了解自定义资源生命周期非常重要,以防止您的数据被删除。
A very interesting and important thing to know is that CloudFormation compares the physical resource id you returned by your Lambda function to the one you returned previously. If the IDs are different, CloudFormation assumes the resource has been replaced with a new resource. Then something interesting happens.
When the resource update logic completes successfully, a Delete request is sent with the old physical resource id. If the stack update fails and a rollback occurs, the new physical resource id is sent in the Delete event.
您可以阅读更多here关于自定义资源生命周期和其他最佳实践的内容