如何避免在 AWS CloudFormation 中使用 Lambda 事件通知时出现的“Unable to validate the following destination configurations”错误?

上次更新时间:2020 年 1 月 16 日

部署 AWS CloudFormation 模板时,我的堆栈出现故障。然后,我收到类似以下内容的错误:“Unable to validate the following destination configurations”。

简短描述

使用以下资源部署 AWS CloudFormation 模板时,您收到此错误:

Amazon S3 在创建存储桶时必须验证通知配置。通过检查存储桶是否具有将事件推送到 Lambda 函数的权限来完成验证。权限资源(通过此检查必须存在的权限资源)需要存储桶名称。这也就表示,权限资源与存储桶互相依赖。

注意:如果您尝试通过类似于以下示例的代码来实施 DependsOn 资源属性以解决此问题,则会收到“Circular dependency between resources”错误。

以下代码示例显示了 S3 存储桶资源与 Lambda 权限资源的 SourceArn 属性之间的循环依赖关系。

"Resources": {
  "MyS3BucketPermission": {
    "Type": "AWS::Lambda::Permission",
    "Properties": {
      "Action": "lambda:InvokeFunction",
      ...
      ...
      "SourceArn": {
        "Ref": "MyS3Bucket"
      }
    }
  },
  "MyS3Bucket": {
    "DependsOn" : "MyS3BucketPermission",
    ...
    ...

注意:最佳实践是向 Amazon S3 事件源的 Lambda 权限资源添加 SourceAccount 属性,这是因为 Amazon S3 的 Amazon 资源名称 (ARN) 不包含账户 ID。SourceArn 属性足以满足大多数其他事件源的需求,但对于 Amazon S3 事件源,请考虑添加 SourceAccount 属性。这样可以防止以下情况:在您删除 S3 存储桶后,发现他人重建了该存储桶,从而向该存储桶的新所有者授予调用您的 Lambda 函数的完全权限。

解决方法

您可以使用 Fn::Join 内部函数与堆栈参数来避免循环依赖。

在以下示例模板中,S3 存储桶名称 BucketPrefix 作为参数提供给 AWS::S3::BucketAWS::Lambda::Permission 资源。

注意:以下示例假定您的 AWS 账户之前并未使用过该存储桶名称。如果您需要重复使用包含此代码段的模板,则每次使用时都必须提供不同的存储桶前缀。

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Parameters": {
    "BucketPrefix": {
      "Type": "String",
      "Default": "test-bucket-name"
    }
  },
  "Resources": {
    "EncryptionServiceBucket": {
      "DependsOn": "LambdaInvokePermission",
      "Type": "AWS::S3::Bucket",
      "Properties": {
        "BucketName": {
          "Fn::Sub": "${BucketPrefix}-encryption-service"
        },
        "NotificationConfiguration": {
          "LambdaConfigurations": [
            {
              "Function": {
                "Fn::GetAtt": [
                  "AppendItemToListFunction",
                  "Arn"
                ]
              },
              "Event": "s3:ObjectCreated:*",
              "Filter": {
                "S3Key": {
                  "Rules": [
                    {
                      "Name": "suffix",
                      "Value": "zip"
                    }
                  ]
                }
              }
            }
          ]
        }
      }
    },
    "LambdaInvokePermission": {
      "Type": "AWS::Lambda::Permission",
      "Properties": {
        "FunctionName": {
          "Fn::GetAtt": [
            "AppendItemToListFunction",
            "Arn"
          ]
        },
        "Action": "lambda:InvokeFunction",
        "Principal": "s3.amazonaws.com",
        "SourceAccount": {
          "Ref": "AWS::AccountId"
        },
        "SourceArn": { 
            "Fn::Sub": "arn:aws:s3:::${BucketPrefix}-encryption-service"
        }
      }
    },
    "AppendItemToListFunction": {
      "Type": "AWS::Lambda::Function",
      "Properties": {
        "Handler": "index.handler",
        "Role": {
          "Fn::GetAtt": [
            "LambdaExecutionRole",
            "Arn"
          ]
        },
        "Code": {
          "ZipFile": {
            "Fn::Join": [
              "",
              [
                "var response = require('cfn-response');",
                "exports.handler = function(event, context) {",
                " var responseData = {Value: event.ResourceProperties.List};",
                " responseData.Value.push(event.ResourceProperties.AppendedItem);",
                " response.send(event, context, response.SUCCESS, responseData);",
                "};"
              ]
            ]
          }
        },
        "Runtime": "nodejs8.10"
      }
    },
    "LambdaExecutionRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "lambda.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Path": "/",
        "Policies": [
          {
            "PolicyName": "root",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "logs:*"
                  ],
                  "Resource": "arn:aws:logs:*:*:*"
                }
              ]
            }
          }
        ]
      }
    }
  }
}

此模板可以避免循环依赖,因为它会按以下顺序创建资源:

  1. AWS Identity and Access Management (IAM) 角色
  2. Lambda 函数
  3. Lambda 权限
  4. S3 存储桶

现在,Amazon S3 可正常验证通知配置并创建存储桶。

您还可以尝试以下解决方法:

  • 创建没有通知配置的 S3 存储桶,然后将该存储桶添加到下一次堆栈更新中。
  • 创建约束较少的 Lambda 权限。例如,通过省略 SourceArn 来允许调用特定 AWS 账户。
  • 创建自定义资源以在堆栈工作流程结束时运行。所有其他资源均已创建后,此资源会将通知配置添加到 S3 存储桶。