AWS CloudFormation で、AWS Lambda のアクセス許可とターゲットグループのリソースとの間の循環依存関係を修正する方法を教えてください。

所要時間6分
0

AWS CloudFormation で、AWS Lambda のアクセス許可 (AWS::Lambda::Permission) とターゲットグループのリソース (AWS::ElasticLoadBalancingV2::TargetGroup) 間で発生する循環依存関係を修正したいです。

解決策

Lambda 関数のターゲットおよび、関連する AWS::Lambda::Permission リソースを使用して AWS::ElasticLoadBalancingV2::TargetGroup を設定する際に、循環依存関係が発生する可能性があります。次の相互依存関係が、このような状況の原因となります。

Targets プロパティを使用して Lambda 関数をターゲットとして登録するには、AWS::ElasticLoadBalancingV2::TargetGroup には、Elastic Load Balancing がその Lambda 関数を呼び出せるようにするための AWS::Lambda::Permission が必要です。

さらに、AWS::Lambda::Permission の SourceArn プロパティには、呼び出しアクセス許可を特定のターゲットグループに制限するために、AWS::ElasticLoadBalancingV2::TargetGroup の ARN が必要です。

このケースでは、AWS::Lambda::Permission が欠けている場合、AWS::ElasticLoadBalancingV2::TargetGroup を完全に作成することはできません。ただし、AWS::Lambda::Permission は、AWS::ElasticLoadBalancingV2::TargetGroup の ARN が欠けていると作成できません。その結果、CloudFormation が最初に作成するリソースを決定できなくなります。この状況が、循環依存エラーです。

この循環依存を修正するには、AWS::ElasticLoadBalancingV2::TargetGroup の Targets プロパティを Lambda ベースのカスタムリソースに置き換え、Lambda 関数をターゲットとして登録します。

その Lambda ベースのカスタムリソースを使用して、Lambda 関数をターゲットグループのターゲットとして登録します。

CloudFormation テンプレートを使用して Lambda ベースのカスタムリソースを定義し、Lambda 関数をターゲットグループのターゲットとして登録します。

テンプレートを作成するときは、次の点に留意してください。

  • CloudFormation テンプレートには、参考用に HelloWorld Lambda 関数および、ELBv2 リソースの例が含まれています。
  • このテンプレートは、追加の RegisterTargetsFunction Lambda 関数および、関連する実行ロールとカスタムリソースをプロビジョニングします。カスタムリソースは、カスタムリソースが作成、更新、または削除されるたびに Lambda 関数を呼び出します。
  • カスタムリソースの作成時には、RegisterTargetsFunction Lambda 関数は定義された 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 コンソールの [出力] セクションに移動すると次のメッセージが表示されます。
    Lambda が、正常にターゲットとして追加されました(arn:aws:lambda:<region>:<account_id>:function:<function_name>)
  5. ターゲットグループをチェックして、ターゲットが登録されたことを確認します。
AWS公式
AWS公式更新しました 3ヶ月前
コメントはありません

関連するコンテンツ