CloudFormation创建ASG时无法向Lambda发布测试消息求助
我尝试通过CloudFormation创建包含1台Spot实例的Auto Scaling Group(ASG),并让实例通过生命周期挂钩关联的Lambda函数自动挂载指定EBS卷,使用的CloudFormation模板如下:
AWSTemplateFormatVersion: '2010-09-09' Description: Auto Scaling with Spot Instances and Lifecycle Hooks Parameters: ImageId: Type: String Default: 'ami-xxxxxx' Description: AMI ID for the instances. VolumeId: Type: String Default: 'vol-xxxxxxxxx' Description: EBS Volume ID to attach. AvailabilityZone: Type: String Default: 'xx-xxxxxxx-xx' Description: Availability Zone for the instance. SecurityGroup: Type: String Default: 'sg-xxxxxxxxx' Description: Security Group ID for the instances. KeyName: Type: String Default: 'xxxxxxxxxx' Description: The EC2 Key Pair to allow SSH access to the instances. Resources: MyRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - autoscaling.amazonaws.com - lambda.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: MyPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:AttachVolume - ec2:DetachVolume Resource: '*' MyAutoScalingGroup: Type: 'AWS::AutoScaling::AutoScalingGroup' Properties: AvailabilityZones: - !Ref AvailabilityZone LaunchTemplate: LaunchTemplateId: !Ref MyLaunchTemplate Version: !GetAtt MyLaunchTemplate.LatestVersionNumber MinSize: '1' MaxSize: '1' DesiredCapacity: '1' MyLaunchTemplate: Type: 'AWS::EC2::LaunchTemplate' Properties: LaunchTemplateData: ImageId: !Ref ImageId InstanceType: t2.xlarge KeyName: !Ref KeyName SecurityGroupIds: - !Ref SecurityGroup InstanceMarketOptions: MarketType: spot SpotOptions: MaxPrice: '0.1' LifecycleHookFunction: Type: 'AWS::Lambda::Function' Properties: Handler: index.handler Role: !GetAtt MyRole.Arn Code: ZipFile: | import boto3 import time def handler(event, context): ec2 = boto3.client('ec2') if event['detail-type'] == 'EC2 Instance-launch Lifecycle Action': # Retry logic to ensure volume is detached before attaching while True: response = ec2.describe_volumes(VolumeIds=[event['detail']['VolumeId']]) if response['Volumes'][0]['State'] == 'available': break time.sleep(10) ec2.attach_volume(VolumeId=event['detail']['VolumeId'], InstanceId=event['detail']['EC2InstanceId'], Device='/dev/sdf') elif event['detail-type'] == 'EC2 Instance-terminate Lifecycle Action': ec2.detach_volume(VolumeId=event['detail']['VolumeId'], InstanceId=event['detail']['EC2InstanceId']) return 'Success' Runtime: python3.8 Timeout: 120 LifecycleHook: Type: 'AWS::AutoScaling::LifecycleHook' Properties: AutoScalingGroupName: !Ref MyAutoScalingGroup LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING NotificationTargetARN: !GetAtt LifecycleHookFunction.Arn RoleARN: !GetAtt MyRole.Arn HeartbeatTimeout: 3600 DefaultResult: CONTINUE Outputs: AutoScalingGroupName: Description: Name of the Auto Scaling Group Value: !Ref MyAutoScalingGroup
创建栈时出现如下错误:
资源处理程序返回消息:“无法使用IAM角色arn:aws:iam::yyyyy:role/xxxxxxxxxxxxxxxx向通知目标arn:aws:lambda:eu-central-1:yyyy:function:xxxxxxxxxxxxxxxxx发布测试消息。请检查您的目标和角色配置,并重试创建生命周期挂钩。(Service: AutoScaling, Status Code: 400, Request ID: xxxxx-ce2a-4d25-9986-yy)”(RequestToken: xxxxxxx-b982-bb06-0fe2-yy, HandlerErrorCode: InvalidRequest)
这个错误核心原因是AutoScaling服务没有权限调用指定的Lambda函数,同时Lambda未允许AutoScaling作为触发源,另外模板还存在Lambda无法获取VolumeId的逻辑问题,以下是修复步骤:
1. 给IAM角色添加Lambda调用权限
修改MyRole的策略,新增lambda:InvokeFunction权限,确保AutoScaling能调用目标Lambda函数:
Policies: - PolicyName: MyPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:AttachVolume - ec2:DetachVolume Resource: '*' - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt LifecycleHookFunction.Arn
2. 给Lambda函数添加资源策略,允许AutoScaling触发
在模板中新增LambdaPermission资源,明确授权AutoScaling服务调用该Lambda:
LambdaPermission: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref LifecycleHookFunction Action: lambda:InvokeFunction Principal: autoscaling.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' SourceArn: !Sub 'arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${MyAutoScalingGroup}'
3. 修复Lambda获取VolumeId的逻辑
生命周期挂钩事件默认不会传递VolumeId,需要通过NotificationMetadata传递参数,并在Lambda中读取:
- 修改
LifecycleHook属性,添加NotificationMetadata传递VolumeId:
LifecycleHook: Type: 'AWS::AutoScaling::LifecycleHook' Properties: AutoScalingGroupName: !Ref MyAutoScalingGroup LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING NotificationTargetARN: !GetAtt LifecycleHookFunction.Arn RoleARN: !GetAtt MyRole.Arn HeartbeatTimeout: 3600 DefaultResult: CONTINUE NotificationMetadata: !Ref VolumeId # 传递VolumeId参数
- 修正Lambda代码,从
NotificationMetadata读取VolumeId:
import boto3 import time def handler(event, context): ec2 = boto3.client('ec2') volume_id = event['detail']['NotificationMetadata'] instance_id = event['detail']['EC2InstanceId'] if event['detail-type'] == 'EC2 Instance-launch Lifecycle Action': # Retry logic to ensure volume is detached before attaching while True: response = ec2.describe_volumes(VolumeIds=[volume_id]) if response['Volumes'][0]['State'] == 'available': break time.sleep(10) ec2.attach_volume(VolumeId=volume_id, InstanceId=instance_id, Device='/dev/sdf') elif event['detail-type'] == 'EC2 Instance-terminate Lifecycle Action': ec2.detach_volume(VolumeId=volume_id, InstanceId=instance_id) return 'Success'
完整修复后的模板
AWSTemplateFormatVersion: '2010-09-09' Description: Auto Scaling with Spot Instances and Lifecycle Hooks Parameters: ImageId: Type: String Default: 'ami-xxxxxx' Description: AMI ID for the instances. VolumeId: Type: String Default: 'vol-xxxxxxxxx' Description: EBS Volume ID to attach. AvailabilityZone: Type: String Default: 'xx-xxxxxxx-xx' Description: Availability Zone for the instance. SecurityGroup: Type: String Default: 'sg-xxxxxxxxx' Description: Security Group ID for the instances. KeyName: Type: String Default: 'xxxxxxxxxx' Description: The EC2 Key Pair to allow SSH access to the instances. Resources: MyRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: - autoscaling.amazonaws.com - lambda.amazonaws.com Action: 'sts:AssumeRole' Policies: - PolicyName: MyPolicy PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - ec2:AttachVolume - ec2:DetachVolume Resource: '*' - Effect: Allow Action: lambda:InvokeFunction Resource: !GetAtt LifecycleHookFunction.Arn MyAutoScalingGroup: Type: 'AWS::AutoScaling::AutoScalingGroup' Properties: AvailabilityZones: - !Ref AvailabilityZone LaunchTemplate: LaunchTemplateId: !Ref MyLaunchTemplate Version: !GetAtt MyLaunchTemplate.LatestVersionNumber MinSize: '1' MaxSize: '1' DesiredCapacity: '1' MyLaunchTemplate: Type: 'AWS::EC2::LaunchTemplate' Properties: LaunchTemplateData: ImageId: !Ref ImageId InstanceType: t2.xlarge KeyName: !Ref KeyName SecurityGroupIds: - !Ref SecurityGroup InstanceMarketOptions: MarketType: spot SpotOptions: MaxPrice: '0.1' LifecycleHookFunction: Type: 'AWS::Lambda::Function' Properties: Handler: index.handler Role: !GetAtt MyRole.Arn Code: ZipFile: | import boto3 import time def handler(event, context): ec2 = boto3.client('ec2') volume_id = event['detail']['NotificationMetadata'] instance_id = event['detail']['EC2InstanceId'] if event['detail-type'] == 'EC2 Instance-launch Lifecycle Action': # Retry logic to ensure volume is detached before attaching while True: response = ec2.describe_volumes(VolumeIds=[volume_id]) if response['Volumes'][0]['State'] == 'available': break time.sleep(10) ec2.attach_volume(VolumeId=volume_id, InstanceId=instance_id, Device='/dev/sdf') elif event['detail-type'] == 'EC2 Instance-terminate Lifecycle Action': ec2.detach_volume(VolumeId=volume_id, InstanceId=instance_id) return 'Success' Runtime: python3.8 Timeout: 120 LambdaPermission: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !Ref LifecycleHookFunction Action: lambda:InvokeFunction Principal: autoscaling.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' SourceArn: !Sub 'arn:aws:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${MyAutoScalingGroup}' LifecycleHook: Type: 'AWS::AutoScaling::LifecycleHook' Properties: AutoScalingGroupName: !Ref MyAutoScalingGroup LifecycleTransition: autoscaling:EC2_INSTANCE_LAUNCHING NotificationTargetARN: !GetAtt LifecycleHookFunction.Arn RoleARN: !GetAtt MyRole.Arn HeartbeatTimeout: 3600 DefaultResult: CONTINUE NotificationMetadata: !Ref VolumeId Outputs: AutoScalingGroupName: Description: Name of the Auto Scaling Group Value: !Ref MyAutoScalingGroup
内容的提问来源于stack exchange,提问作者Djangonaut

