如何修复 AWS Lambda 权限与 AWS CloudFormation 中目标组资源之间的循环依赖关系?
上次更新时间:2021 年 2 月 2 日
我想修复 AWS CloudFormation 中 AWS Lambda 权限 (AWS::Lambda::Permission) 和目标组资源 (AWS::ElasticLoadBalancingV2::TargetGroup) 之间的循环依赖关系。
简短描述
如果您提供 AWS::Lambda::Permission 给 AWS::ElasticLoadBalancingV2::TargetGroup 而不是 LoadBalancer 来限制 Lambda 函数的访问权限,则会获得循坏依赖关系。这是因为在将 Lambda 函数关联到要创建的目标组之前,必须具有 Lambda 权限。创建 Lambda 权限时,您必须将权限与 AWS 委托人和 SourceARN(在本例中为目标组)关联。这就是为什么目标组在创建 Lambda 权限之前必须存在。要修复此循环依赖关系,您可以使用 Lambda 支持的自定义资源。
解决方法
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 函数显示一个 HTML 页面,其中包含消息 "Hello from Lambda!"。
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 函数执行注册目标 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_target 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. 将 S3BucketName、子网、VpcId 和 TargetGroupName 参数的值传递到您在步骤 7 中使用模板创建的 AWS CloudFormation 堆栈。
9. 创建堆栈。
10. 完成所有 API 调用后,转到 AWS CloudFormation 控制台的输出部分,然后查找以下消息:
Successfully Added Lambda(arn:aws:lambda:us-east-1:123456789:function:testLambda) as Target