Saltar al contenido

¿Cómo soluciono la dependencia circular entre un permiso de AWS Lambda y los recursos del grupo de destino en AWS CloudFormation?

8 minutos de lectura
0

Quiero corregir la dependencia circular entre un permiso de AWS Lambda (AWS::Lambda::Permission) y los recursos del grupo de destino (AWS::ElasticLoadBalancingV2::TargetGroup) en AWS CloudFormation.

Resolución

Puede producirse una dependencia circular al configurar AWS::ElasticLoadBalancingV2::TargetGroup con un objetivo de función de Lambda y un recurso AWS::Lambda::Permission asociado. Esta situación se produce debido a las siguientes interdependencias:

Para registrar una función de Lambda como objetivo mediante su propiedad Targets, AWS::ElasticLoadBalancingV2::TargetGroup requiere el permiso AWS::Lambda::Permission para permitir que Elastic Load Balancing invoque la función de Lambda.

Además, AWS::Lambda::Permission requiere el ARN de AWS::ElasticLoadBalancingV2::TargetGroup en su propiedad SourceArn para restringir los permisos de invocación a grupos objetivo específicos.

En este caso, AWS::ElasticLoadBalancingV2::TargetGroup no se puede crear por completo sin el permiso AWS::Lambda::Permission. Sin embargo, el permiso AWS::Lambda::Permission no se puede crear sin el ARN de AWS::ElasticLoadBalancingV2::TargetGroup. Como resultado, CloudFormation no puede determinar el recurso que se debe crear primero. Esta situación es un error de dependencia circular.

Para corregir esta dependencia circular, sustituya la propiedad Targets de AWS::ElasticLoadBalancingV2::TargetGroup por un recurso personalizado respaldado por Lambda para registrar la función de Lambda como objetivo.

Uso de un recurso personalizado respaldado por Lambda para registrar la función de Lambda como objetivo en el grupo objetivo

Use una plantilla de CloudFormation para definir un recurso personalizado respaldado por Lambda para registrar su función de Lambda como objetivo en su grupo objetivo.

Tenga en cuenta los siguientes factores al crear la plantilla:

  • La plantilla de CloudFormation incluye un ejemplo de la función de Lambda HelloWorld y los recursos de ELBv2 como referencia.
  • Esta plantilla proporciona una función de Lambda RegisterTargetsFunction adicional y un rol de ejecución asociado y un recurso personalizado. El recurso personalizado invoca la función de Lambda cada vez que se crea, actualiza o elimina el recurso personalizado.
  • Durante la creación del recurso personalizado, la función de Lambda RegisterTargetsFunction registra la función de Lambda definida como el objetivo en el grupo objetivo proporcionado. Durante la eliminación de recursos personalizados, la función anula el registro del objetivo.
  • Puede modificar la plantilla con su propio código.
  • Esta plantilla utiliza recursos personalizados y respaldados por AWS Lambda y asume que conoce las prácticas recomendadas de Lambda y la resolución de problemas.

Creación de la plantilla de CloudFormation

Para crear la plantilla de CloudFormation, utilice el siguiente ejemplo:

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

Uso de la plantilla de CloudFormation para crear su pila de CloudFormation

  1. Guarde la plantilla que ha creado como un archivo YAML.
  2. Utilice la AWS CLI o la consola de CloudFormation para crear una pila.
    Nota: Si se muestran errores al ejecutar comandos de la Interfaz de la línea de comandos de AWS (AWS CLI), consulte Solución de errores de la AWS CLI. Además, asegúrese de utilizar la versión más reciente de la AWS CLI.
  3. Utilice los parámetros de pila VpcId, Subnets y TargetGroupName para especificar la Amazon Virtual Private Cloud (Amazon VPC), las subredes y el nombre del grupo objetivo que desee.
  4. Cuando la pila alcance el estado CREATE_COMPLETE, vaya a la sección Salidas de la consola de AWS CloudFormation para ver el siguiente mensaje:
    «Se agregó correctamente Lambda (arn:aws:lambda:<region>:<account_id>:function:<function_name>) como objetivo»
  5. Compruebe el grupo objetivo para comprobar que el objetivo estaba registrado.
OFICIAL DE AWSActualizada hace un año