跳至内容

如何修复 AWS CloudFormation 中 AWS Lambda 权限与目标组资源之间的循环依赖?

6 分钟阅读
0

我想修复 AWS CloudFormation 中 AWS Lambda 权限 (AWS::Lambda::Permission) 与目标组资源 (AWS::ElasticLoadBalancingV2::TargetGroup) 之间的循环依赖。

解决方法

当您为 AWS::ElasticLoadBalancingV2::TargetGroup 配置 Lambda 函数目标和关联的 AWS::Lambda::Permission 资源时,可能会发生循环依赖。出现此情况是由于以下相互依赖关系造成的:

要使用 Lambda 函数的 Targets 属性将该函数注册为目标,AWS::ElasticLoadBalancingV2::TargetGroup 需要 AWS::Lambda::Permission 来允许弹性负载均衡调用 Lambda 函数。

此外,AWS::Lambda::Permission 在其 SourceArn 属性中需要 AWS::ElasticLoadBalancingV2::TargetGroup 的 ARN,以便将调用权限限制为特定的目标组。

在这种情况下,如果没有 AWS::Lambda::Permission,将无法完整创建 AWS::ElasticLoadBalancingV2::TargetGroup。但是,如果没有 AWS::ElasticLoadBalancingV2::TargetGroup 的 ARN,AWS::Lambda::Permission 也无法创建。因此,CloudFormation 无法确定要先创建的资源。这种情况为循环依赖错误。

要解决这种循环依赖,请将 AWS::ElasticLoadBalancingV2::TargetGroup 的 Targets 属性替换为由 Lambda 支持的自定义资源,以便将 Lambda 函数注册为目标。

使用由 Lambda 支持的自定义资源将您的 Lambda 函数注册为目标组中的一个目标

使用 CloudFormation 模板定义一个由 Lambda 支持的自定义资源,以将您的 Lambda 函数注册为目标组中的目标。

在构建模板时,请记住以下因素:

  • CloudFormation 模板包含一个示例 Lambda 函数 HelloWorldELBv2 资源,以供参考。
  • 该模板预置了一个额外的 Lambda 函数 RegisterTargetsFunction,以及一个关联的执行角色和一个自定义资源。每当创建、更新或删除自定义资源时,自定义资源都会调用 Lambda 函数。
  • 在创建自定义资源期间,Lambda 函数 RegisterTargetsFunction 会将定义的 Lambda 函数注册为提供的目标组中的目标。在删除自定义资源期间,该函数会取消注册该目标。
  • 您可以使用您自己的代码修改模板。
  • 此模板使用由 AWS Lambda 支持的自定义资源,并假设您熟悉 Lambda 的最佳实践问题故障排除

创建 CloudFormation 模板

要创建您的 CloudFormation 模板,请使用以下示例:

Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

AWSTemplateFormatVersion: 2010-09-09
Description: HelloWorld Lambda function template for Application Load Balancer Lambda as target
Parameters:
  # VPC in which the LoadBalancer and the LoadBalancer SecurityGroup will be created
  VpcId:
      Type: AWS::EC2::VPC::Id
  # Subnets in which the LoadBalancer will be created.
  Subnets:
    Type: List<AWS::EC2::Subnet::Id>
  # Name of the TargetGroup
  TargetGroupName:
    Type: String
    Default: 'MyTargets'

Resources:
  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: AllowRegisterAndDeregisterTargets
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - 'elasticloadbalancing:RegisterTargets'
            Resource: !GetAtt TargetGroup.TargetGroupArn
          - Effect: Allow
            Action:
            - 'elasticloadbalancing:DeregisterTargets'
            Resource: '*'
  # Lambda function which displays an HTML page with "Hello from Lambda!" message upon invocation
  HelloWorldFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          def lambda_handler(event, context):
              return {
                "statusCode": 200,
                "statusDescription": "HTTP OK",
                "isBase64Encoded": False,
                "headers": {
                  "Content-Type": "text/html"
                },
                "body": "<h1>Hello from Lambda!</h1>"
              }
      MemorySize: 128
      Handler: index.lambda_handler
      Timeout: 30
      Runtime: python3.12
      Role: !GetAtt LambdaExecutionRole.Arn
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Scheme: internet-facing
      Subnets: !Ref Subnets
      SecurityGroups:
      - !Ref LoadBalancerSecurityGroup
  HttpListener:
    Type: 'AWS::ElasticLoadBalancingV2::Listener'
    Properties:
      DefaultActions:
      - TargetGroupArn: !Ref TargetGroup
        Type: forward
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP to client host
      VpcId: !Ref VpcId
      SecurityGroupIngress:
      - IpProtocol: tcp
        FromPort: 80
        ToPort: 80
        CidrIp: 0.0.0.0/0
  HelloWorldFunctionInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt HelloWorldFunction.Arn
      Action: 'lambda:InvokeFunction'
      Principal: elasticloadbalancing.amazonaws.com
      SourceArn: !GetAtt TargetGroup.TargetGroupArn
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Ref TargetGroupName
      TargetType: lambda
  # Custom resource for Lambda function - "RegisterTargetsFunction"
  RegisterTargets:
    DependsOn: [TargetGroup, HelloWorldFunctionInvokePermission]
    Type: Custom::RegisterTargets
    Properties:
      ServiceToken: !GetAtt RegisterTargetsFunction.Arn
      ServiceTimeout: 15
      # Input parameters for the Lambda function
      LambdaFunctionARN: !GetAtt HelloWorldFunction.Arn
      TargetGroupARN: !GetAtt TargetGroup.TargetGroupArn
  # Lambda function that performs RegisterTargets API call
  RegisterTargetsFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import logging
          import boto3, json, botocore
          import cfnresponse
          #Define logging properties for 'logging'
          log = logging.getLogger()
          log.setLevel(logging.INFO)
          #Main Lambda function to be executed
          def lambda_handler(event, context):
              try:
                  # print the event type sent from cloudformation
                  log.info ("'" + str(event['RequestType']) + "' event was sent from CFN")
                  # Input Parameters
                  TargetGroupARN = event['ResourceProperties']['TargetGroupARN']
                  LambdaFunctionARN = event['ResourceProperties']['LambdaFunctionARN']
                  log.info("TargetGroup ARN value is:" + TargetGroupARN)
                  log.info("Lambda Function ARN value is:" + LambdaFunctionARN)
                  responseData = {}
                  # ELBV2 initilize
                  client = boto3.client('elbv2')
                  # Initilize Vars
                  response = ''
                  error_msg = ''
                  if event['RequestType'] == "Create" or event['RequestType'] == "Update":
                      # Make the RegisterTargets API call
                      try:
                          response = client.register_targets(
                              TargetGroupArn=TargetGroupARN,
                              Targets=[
                                  {
                                      'Id': LambdaFunctionARN
                                  },
                              ]
                          )
                      except botocore.exceptions.ClientError as e:
                          error_msg = str(e)
                      if error_msg:
                          log.info("Error Occured:" + error_msg)
                          response_msg = error_msg
                          # SIGNAL BACK TO CLOUDFORMATION
                          log.info("Trying to signal FAILURE back to cloudformation.")
                          responseData = {"Message" : response_msg, "Function" : context.log_stream_name}
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
                      else:
                          response_msg = "Successfully registered Lambda(" +  LambdaFunctionARN + ") as Target"
                          log.info(response_msg)
                          # SIGNAL BACK TO CLOUDFORMATION
                          log.info("Trying to signal SUCCESS back to cloudformation.")
                          responseData = {"Message" : response_msg, "Function" : context.log_stream_name}
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)

                      # log the end of the create event
                      log.info ("End of '" + str(event['RequestType']) + "' Event")
                  elif "Delete" in str(event['RequestType']):
                      # Make the DeregisterTargets API call
                      try:
                          response = client.deregister_targets(
                              TargetGroupArn=TargetGroupARN,
                              Targets=[
                                  {
                                      'Id': LambdaFunctionARN
                                  },
                              ]
                          )
                      except botocore.exceptions.ClientError as e:
                          error_msg = str(e)
                      if error_msg:
                          log.info("Error Occured:" + error_msg)
                          response_msg = error_msg
                          # SIGNAL BACK TO CLOUDFORMATION
                          log.info("Trying to signal FAILURE back to cloudformation.")
                          responseData = {"Message" : response_msg, "Function" : context.log_stream_name}
                          cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
                      else:
                          response_msg = "Successfully deregistered Lambda(" +  LambdaFunctionARN + ") as Target"
                          log.info(response_msg)
                          # SIGNAL BACK TO CLOUDFORMATION
                          log.info("Trying to signal SUCCESS back to cloudformation.")
                          responseData = {"Message" : response_msg, "Function" : context.log_stream_name}
                          cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)

                      # log the end of the create event
                      log.info ("End of '" + str(event['RequestType']) + "' Event")
                  else:
                      log.info ("RequestType sent from cloudformation is invalid.")
                      log.info ("Was expecting 'Create', 'Update' or 'Delete' RequestType(s).")
                      log.info ("The detected RequestType is : '" + str(event['RequestType']) + "'")

                      # SIGNAL BACK TO CLOUDFORMATION
                      log.info("Trying to signal FAILURE back to cloudformation due to invalid request type.")
                      responseData={"Function" : context.log_stream_name}
                      cfnresponse.send(event, context, cfnresponse.FAILED, responseData)


              except Exception as e:
                  log.info ("Function failed due to the following error:")
                  print (e)

                  # SIGNAL BACK TO CLOUDFORMATION
                  log.info("Trying to signal FAILURE back to cloudformation due to the error.")
                  responseData={"Function" : context.log_stream_name}
                  cfnresponse.send(event, context, cfnresponse.FAILED, responseData)
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: python3.12
      Timeout: '15'

Outputs:
  Message:
    Description: Message returned from Lambda
    Value: !GetAtt
      - RegisterTargets
      - Message

使用 CloudFormation 模板构建 CloudFormation 堆栈

  1. 将您创建的模板保存为 YAML 文件。
  2. 使用 AWS CLI 或 CloudFormation 控制台创建堆栈
    **注意:**如果您在运行 AWS 命令行界面 (AWS CLI) 命令时收到错误,请参阅 AWS CLI 错误故障排除。此外,请确保您使用的是最新版本的 AWS CLI
  3. 使用 VpcIdSubnetsTargetGroupName 堆栈参数指定所需的 Amazon Virtual Private Cloud (Amazon VPC)、子网和目标组名称。
  4. 在堆栈达到 CREATE_COMPLETE 状态后,转到 AWS CloudFormation 控制台的 Outputs(输出)部分,以查看以下消息:
    “成功添加了 Lambda(arn:aws:lambda:<region>:<账户 ID>:function:<函数名称>)作为目标”
  5. 检查目标组以验证目标是否已注册。
AWS 官方已更新 9 个月前