AWS CloudFormation で AWS Lambda のアクセス許可とターゲットグループリソース間の循環依存関係を修正するにはどうすればよいですか?

最終更新日: 2021 年 2 月 2 日

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

簡単な説明

Lambda 関数へのアクセスを制限するために、AWS::Lambda::PermissionLoadBalancer ではなく AWS::ElasticLoadBalancingV2::TargetGroup へのアクセス許可を提供すると、循環依存関係が発生します。これは、作成するターゲットグループに Lambda 関数を関連付ける前に、Lambda アクセス許可が存在する必要があるためです。Lambda アクセス許可を作成する際には、AWS Principal および SourceARN (この場合はターゲットグループ) にアクセス許可を関連付ける必要があります。そのため、Lambda アクセス許可を作成する前に、ターゲットグループが存在する必要があります。この循環依存関係を修正するには、Lambda-backed カスタムリソースを使用できます。

解決方法

1.    次の内容を含む index.py という名前のファイルを作成します。

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.

def handler(event, context): 
    return {
      "statusCode": 200,
      "statusDescription": "HTTP OK",
      "isBase64Encoded": False,
      "headers": {
         "Content-Type": "text/html"
      },
      "body": "<h1>Hello from Lambda!</h1>"
    }

注: index.py の AWS Lambda 関数は、関数が呼び出されるときに「Hello from Lambda!」というメッセージが記載された HTML ページを表示します。

2.    index.py ファイルを website.zip という名前のアーカイブファイルに追加します。

3.    index.py の.zip ファイルを作成するには、次のコマンドを実行します。

$zip website.zip index.py

4.    AWS GitHub の AWS CloudFormation テンプレートに基づいて cfnresponse.py という名前のファイルを作成します。

5.    次の内容に基づいて custom.py というファイルを作成します。

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.

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:
        if event['RequestType'] == "Create" or event['RequestType'] == "Update":
            # print the event type sent from cloudformation
            log.info ("'" + str(event['RequestType']) + "' event was sent from CFN")
            # Imput 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 = ''
            # Make the 1st API call to get the Lambda policy and extract SID of the initial permissions that were created by the CFN template.
            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
                # TODO: 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 Added Lambda(" +  LambdaFunctionARN + ") as Target"
                log.info(response_msg)
                # TODO: 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']):
            # print the event type sent from cloudformation
            log.info ("'Delete' event was sent from CFN")
            
            # TODO: DELETE THINGS
            log.info("TODO: DELETE THINGS")
            
            # TODO: SIGNAL BACK TO CLOUDFORMATION
            log.info("Trying to signal SUCCESS back to cloudformation.")
            responseData = {"Function" : context.log_stream_name}
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData)
            
            # log the end of the Delete event
            log.info ("End of 'Delete' 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']) + "'")
            
            #TODO: 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)
        
        #TODO: 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)

注: custom.py の Lambda 関数は RegisterTargets API 呼び出しを実行し、その API 呼び出しが完了すると AWS CloudFormation に通知します。

6.    cfnresponse.py ファイルと custom.py ファイルを custom.zip という名前のアーカイブファイルに追加します。例:

zip custom.zip cfnresponse.py custom.py

注: custom.py ファイルは、Boto3 API 呼び出しの RegisterTargets を使用します。この API 呼び出しは、指定されたターゲットを、指定されたターゲットグループに登録します。詳細については、Boto3 ウェブサイトの register_targets Python スニペットをご参照ください。cfnresponse.py ファイルには、ログと通知を Amazon CloudWatch および AWS CloudFormation にプッシュする関数が含まれています。

7.    次の YAML テンプレートを使用して AWS 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'
  # Name of the S3 bucket where custom.zip and website.zip are uploaded to 
  S3BucketName:
    Type: String
    Default: you-s3-bucket-name
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: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
            - '*'
            Resource: '*'
  # Lambda function which displays an HTML page with "Hello from Lambda!" message upon invocation
  HelloWorldFunction1234:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        S3Bucket: !Ref S3BucketName
        S3Key: website.zip
      FunctionName: testLambda
      MemorySize: 128
      Handler: index.handler
      Timeout: 30
      Runtime: python3.7
      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 HelloWorldFunction1234.Arn
      Action: 'lambda:InvokeFunction'
      Principal: elasticloadbalancing.amazonaws.com
      SourceArn: !Sub 
        - >-
          arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:${TGFullName}
        - TGFullName: !GetAtt TargetGroup.TargetGroupFullName
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Ref TargetGroupName
      TargetType: lambda
  # Custom resource for Lambda function - "HelloWorldFunction1234"
  HelloWorld:
    DependsOn: [TargetGroup, HelloWorldFunctionInvokePermission]
    Type: Custom::HelloWorld
    Properties:
      ServiceToken: !GetAtt TestFunction.Arn
      # Input parameters for the Lambda function
      LambdaFunctionARN: !GetAtt HelloWorldFunction1234.Arn
      TargetGroupARN: !Sub 
        - >-
          arn:aws:elasticloadbalancing:${AWS::Region}:${AWS::AccountId}:${TGFullName}
        - TGFullName: !GetAtt TargetGroup.TargetGroupFullName
  # Lambda function that performs RegisterTargets API call
  TestFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: !Ref S3BucketName
        S3Key: custom.zip
      Handler: custom.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Runtime: python3.7
      Timeout: '5'
Outputs:
  Message:
    Description: Message returned from Lambda
    Value: !GetAtt 
      - HelloWorld
      - Message

8.    ステップ 7 で準備したテンプレートを使用して作成した AWS CloudFormation スタックに、S3BucketNameSubnetsVpcId、および TargetGroupName パラメータの値を渡します。

9.    スタックを作成します。

10.    すべての API 呼び出しが完了したら、AWS CloudFormation コンソールの [Outputs] (出力) セクションに移動し、次のメッセージを探します。

Successfully Added Lambda(arn:aws:lambda:us-east-1:123456789:function:testLambda) as Target

この記事はお役に立ちましたか?


請求に関するサポートまたは技術サポートが必要ですか?