保留不健康的 Auto Scaling 实例 -- 分离不健康的 ASG 实例而不是终止

3 分钟阅读
内容级别:高级
3

这篇文章提供了一种基于Lambda的方法,用于保留AWS自动伸缩组(ASGs)中的不健康实例,而不是自动终止它们,同时通过自动启动新的替换实例来维持ASG的性能。这里利用CloudFormation实现简单快速部署,旨在通过用户友好的配置自动保留不健康的ASG实例以便后续故障分析等。

背景:

AWS自动伸缩组(ASG)监控实例的健康状态,并自动替换不健康的实例以确保可靠性和可用性,这一过程会自动终止不健康的实例并启动新的实例作为替代。

客户需求:

在某些情况下,客户需要保留不健康的实例以进行进一步分析。例如,如果一个实例被弹性负载均衡器(ELB)标记为不健康,客户可能希望调查故障的原因。同样,可能需要从实例中提取日志或其他数据以了解出了什么问题。

当前限制:

ASG的默认行为不允许保留不健康的实例。虽然生命周期钩子可以延迟终止,但它们只提供临时延迟(最多2小时)。延长这一期限需要连续的API调用(RecordLifecycleActionHeartbeat),这不是一个用于长期保留不健康实例的理想或可持续的解决方案。

解决方案:

为了解决这一挑战,我写了一个Lambda函数,有效地绕过了不健康实例的自动替换。该函数通过暂停ASG设置中的“替换不健康”过程来工作。然后,它每分钟监控ASG中实例的健康状态。一旦检测到不健康的实例,Lambda函数就会将其从ASG中分离,防止其被终止(在EC2控制台中检查不健康的实例),并自动启动一个新的替代实例。可选地,它还可以通过SNS通知客户,并提供所采取的行动和受影响的实例的详细信息。这个解决方案不仅保留了用于调查的不健康实例,而且确保只有健康的实例处于活动状态并提供服务,维护了ASG的完整性和性能。

部署时,Lambda需要一个IAM角色和相关权限来工作,还需要EventBridge每分钟触发Lambda。Lambda还可以发送SNS消息。手动配置整个解决方案既困难又不客户友好。因此,我编写了一个CloudFormation模板,可以简化并自动配置。客户只需要提供ASG名称(支持多个ASG)和SNS主题ARN(可选)。

CloudFormation 模版内容:

AWSTemplateFormatVersion: '2010-09-09'
Description: 'AWS CloudFormation template to create a Lambda function with optional SNS topic and schedule it to run every minute using EventBridge to preserve unhealthy ASG instances.'

Parameters:
  SNSTopicARN:
    Type: String
    Default: ''
    Description: The ARN of the SNS topic for notifications (optional).

  ASGNames:
    Type: CommaDelimitedList
    Description: A comma-separated list of Auto Scaling Group names to monitor (Support multiple ASGs).

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: LambdaExecutionPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - autoscaling:DescribeAutoScalingGroups
                  - autoscaling:DetachInstances
                  - autoscaling:SuspendProcesses
                  - sns:Publish
                Resource: '*'

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Code:
        ZipFile: |
          import boto3
          from datetime import datetime
          import os

          autoscaling_client = boto3.client('autoscaling')
          sns_client = boto3.client('sns')

          ASG_NAMES = os.getenv('ASG_NAMES', '').split(',')
          SNS_TOPIC_ARN = os.getenv('SNS_TOPIC_ARN')
          MAX_DETACH = 20  # Maximum number of instances to detach at once (ASG API limitation)

          def process_asg(asg_name):
              response = autoscaling_client.describe_auto_scaling_groups(
                  AutoScalingGroupNames=[asg_name]
              )
              asg = response['AutoScalingGroups'][0]
              
              # Check and suspend ReplaceUnhealthy process if not already suspended
              if 'ReplaceUnhealthy' not in [process['ProcessName'] for process in asg['SuspendedProcesses']]:
                  autoscaling_client.suspend_processes(
                      AutoScalingGroupName=asg_name,
                      ScalingProcesses=['ReplaceUnhealthy']
                  )

              return [i['InstanceId'] for i in asg['Instances'] if i['HealthStatus'] != 'Healthy'], asg['DesiredCapacity']

          def lambda_handler(event, context):
              timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
              message_details = []

              for asg_name in ASG_NAMES:
                  unhealthy_instances, desired_capacity = process_asg(asg_name)
                  max_detach_by_percent = max(int(desired_capacity * 0.1),1) # Change 0.1 to different percentage base on your use case; Ensure minimum 1 if there are unhealthy instances
                  max_to_detach = min(max_detach_by_percent, MAX_DETACH, len(unhealthy_instances))
                  to_detach = unhealthy_instances[:max_to_detach]

                  if to_detach:
                      autoscaling_client.detach_instances(
                          InstanceIds=to_detach,
                          AutoScalingGroupName=asg_name,
                          ShouldDecrementDesiredCapacity=False
                      )
                      for instance_id in to_detach:
                          message_details.append(f"Detached unhealthy instance {instance_id} from ASG {asg_name} at {timestamp}")

              if message_details and SNS_TOPIC_ARN:
                  sns_message = "\n".join(message_details)
                  sns_client.publish(
                      TopicArn=SNS_TOPIC_ARN,
                      Message=sns_message,
                      Subject='Unhealthy ASG Instance Detached'
                  )
      Runtime: python3.8
      Timeout: 120
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref SNSTopicARN
          ASG_NAMES: !Join [",", !Ref ASGNames]

  EventRule:
    Type: AWS::Events::Rule
    Properties:
      ScheduleExpression: 'rate(1 minute)'
      Targets:
        - Arn: !GetAtt LambdaFunction.Arn
          Id: "TargetFunction"
  
  LambdaPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !GetAtt LambdaFunction.Arn
      Action: 'lambda:InvokeFunction'
      Principal: 'events.amazonaws.com'
      SourceArn: !GetAtt EventRule.Arn

Outputs:
  LambdaFunctionArn:
    Description: "ARN of the Lambda function"
    Value: !GetAtt LambdaFunction.Arn

以下是如何部署CloudFormation模板的步骤:

步骤一:保存模板

  • 将上述YAML内容保存为名为 keep_unhealthy_ASG_instance.yaml 的文件。

步骤二:访问AWS管理控制台

  • 登录您的AWS账户,并导航至AWS CloudFormation控制台。

步骤三:创建一个新堆栈

  • 选择“创建堆栈” → “使用新资源(标准)”。
  • 选择“上传模板文件”,点击“选择文件”,并上传 keep_unhealthy_ASG_instance.yaml 文件。
  • 点击“下一步”。

步骤四:指定堆栈详细信息

  • 输入堆栈名称,例如 KeepUnhealthyASGInstanceStack。
  • 指定参数,例如ASGNames(要监控的自动伸缩组名称的逗号分隔列表)和用于通知的SNS主题的ARN(可选)。您可以按照此文档配置SNS主题
  • 点击“下一步”。
  • 参考截图:在此处输入图片描述

步骤五:配置堆栈选项

  • 配置任何必要的选项,如标签或权限。对于大多数情况,默认选项已足够。
  • 点击“下一步”。

步骤六:检查并启动:

  • 检查所有设置以确保它们是正确的。
  • 确认AWS CloudFormation可能会创建IAM资源,然后点击“创建堆栈”。

步骤七:查看部署

  • 在CloudFormation控制台上查看堆栈的创建。等待状态变为“CREATE_COMPLETE”。

验证结果:

  • 在CloudFormation部署完成后,您可以去ASG控制台验证“替换不健康”进程已被暂停。您也可以使用以下CLI命令使一个实例变为不健康,然后验证不健康的实例是否保留(在EC2控制台中)以及ASG是否自动启动新实例:
aws autoscaling set-instance-health \
    --instance-id i-061c63c5eb45f0416 \
    --health-status Unhealthy

脚本限制:

  • 实例分离:脚本配置为在单次触发中最多分离ASG所需容量的10%或20个实例(以较低者为准,您也可以根据需求修改这个10%参数),此限制旨在防止大量不健康实例的脱离会降低服务可用性。
  • 健康状态诊断:脚本不提供实例被标记为不健康的具体原因。要诊断实例健康状态的原因,请查看弹性负载均衡器(ELB)和EC2指标。这些指标可以提供实例不健康状态背后的原因。

免责声明:

我已在我的测试环境中测试过这个脚本,并运行良好。然而,请注意这个脚本是尽力开发的,虽然我们力求准确,但不能保证其功能完美。在生产环境中部署前,请务必在测试环境中彻底测试该脚本。如果您对此脚本有疑问,欢迎联系AWS支持团队

profile pictureAWS
支持工程师
Tim
已​发布 1 个月前1167 查看次数