AutoScalingで管理されているEC2に自動的にCloudWatch Alarmを設定するLambda関数を作成してみた

2024.04.30

AutoScalingで管理されているEC2に対してCloudWatch Alarmを設定するLambdaを作成したのでブログに残します。

やること

簡易的ですが構成図を作成しました。

AWSサービスとしてはAmazon EventBridgeAmazon SNSAmazon SQSAWS Lambdaを使用しています。
Amazon EventBridgeでAutoScalingのスケールイン、スケールアウト成功イベントを検知したらAmazon SNSへ連携を行います。
Amazon SNSからAmazon SQSへメッセージを送り、AWS Lambdaで処理を行ってアラームを設定します。
CloudWatch Agentを使用してメモリ使用率などを取得している場合、プロセスの起動が遅くてCloudWatchメトリクスの作成が遅れたりする可能性もあるのでAmazon SQSを使用して処理を開始するまでに遅延を持たせるような構成としました。

作成したコード

作成したコードは以下になります。

import json
import boto3
import os

cloudWatch = boto3.client('cloudwatch')
sns_arn = os.environ['SNS_ARN']

def lambda_handler(event, context):
    body = json.loads(event['Records'][0]['body'])
    Message = json.loads(body['Message'])
    detail_type = Message['detail-type']
    instance_id = Message['detail']['EC2InstanceId']

    if detail_type == "EC2 Instance Launch Successful":
        cleate_alarms(instance_id)
    elif detail_type == "EC2 Instance Terminate Successful":
        cloudWatch.delete_alarms(
            AlarmNames=[
                f'cpu_alarm_{instance_id}',
                f'mem_alarm_{instance_id}'
            ]
        )

def cleate_alarms(instance_id):
    # CPU使用率
    cloudWatch.put_metric_alarm(
        AlarmName=f'cpu_alarm_{instance_id}',
        AlarmDescription='cpu usage over 50%',
        OKActions=[
            sns_arn
        ],
        AlarmActions=[
            sns_arn
        ],
        MetricName='CPUUtilization',
        Namespace='AWS/EC2',
        Statistic='Average',
        Dimensions=[
            {
                'Name':'InstanceId',
                'Value': instance_id
            }
        ],
        Period=300,
        EvaluationPeriods=1,
        DatapointsToAlarm=1,
        Threshold=50,
        ComparisonOperator='GreaterThanOrEqualToThreshold'
    )

    # メモリ使用率
    cloudWatch.put_metric_alarm(
        AlarmName=f'mem_alarm_{instance_id}',
        AlarmDescription='memory usage over 50%',
        OKActions=[
            sns_arn
        ],
        AlarmActions=[
            sns_arn
        ],
        MetricName='mem_used_percent',
        Namespace='CWAgent',
        Statistic='Average',
        Dimensions=[
            {
                'Name':'InstanceId',
                'Value': instance_id
            }
        ],
        Period=60,
        EvaluationPeriods=1,
        DatapointsToAlarm=1,
        Threshold=50,
        ComparisonOperator='GreaterThanOrEqualToThreshold'
    )

コードはPythonで作成しています。
AutoScalingのスケールイベントから「detail-type」と「EC2InstanceId」を取得してCloudWatch Alarmの作成と削除を行うようにしています。
今回はEC2にCloudWatch Agentをインストールしてメモリ使用率も取得したのでメモリ使用率のアラームも作成しています。

AWSリソースの作成

AutoScalingは以下のブログで作成したものを対象とさせていただきます。
Blue/Greenデプロイを行うとAutoScalingグループの接頭辞が「CodeDeploy_」になるのでEventBridgeのイベントパターンでワイルドカードを使用して指定するようにします。

また、EC2にはCloudWatch Agentをインストールしてメモリ使用率を取得するようにしています。
以下のドキュメントの手順でインストールしてからAMIを作成してからCodeDeployを作成してください。
エージェント設定を使用した EC2 インスタンスへの CloudWatch エージェントのインストール

Lambda関数作成

Lambda関数などの作成は以下のCloudFormationで行っています。

CloudFormationテンプレート (ここをクリックしてください)
AWSTemplateFormatVersion: "2010-09-09"

Description: Lambda Stack

Parameters:
# ------------------------------------------------------------#
# Parameters
# ------------------------------------------------------------# 
  Email:
    Type: String

Resources:
# ------------------------------------------------------------#
# Lambda
# ------------------------------------------------------------# 
  LambdaIAMRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaSQSQueueExecutionRole
      Policies:
        - PolicyName: lambda-iam-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - cloudwatch:PutMetricAlarm
                  - cloudwatch:DeleteAlarms
                Resource: "*"

  Lambda:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import boto3
          import os

          cloudWatch = boto3.client('cloudwatch')
          sns_arn = os.environ['SNS_ARN']

          def lambda_handler(event, context):
              body = json.loads(event['Records'][0]['body'])
              Message = json.loads(body['Message'])
              detail_type = Message['detail-type']
              instance_id = Message['detail']['EC2InstanceId']

              if detail_type == "EC2 Instance Launch Successful":
                  cleate_alarms(instance_id)
              elif detail_type == "EC2 Instance Terminate Successful":
                  cloudWatch.delete_alarms(
                      AlarmNames=[
                          f'cpu_alarm_{instance_id}',
                          f'mem_alarm_{instance_id}'
                      ]
                  )

          def cleate_alarms(instance_id):
              # CPU使用率
              cloudWatch.put_metric_alarm(
                  AlarmName=f'cpu_alarm_{instance_id}',
                  AlarmDescription='cpu usage over 50%',
                  OKActions=[
                      sns_arn
                  ],
                  AlarmActions=[
                      sns_arn
                  ],
                  MetricName='CPUUtilization',
                  Namespace='AWS/EC2',
                  Statistic='Average',
                  Dimensions=[
                      {
                          'Name':'InstanceId',
                          'Value': instance_id
                      }
                  ],
                  Period=300,
                  EvaluationPeriods=1,
                  DatapointsToAlarm=1,
                  Threshold=50,
                  ComparisonOperator='GreaterThanOrEqualToThreshold'
              )

              # メモリ使用率
              cloudWatch.put_metric_alarm(
                  AlarmName=f'mem_alarm_{instance_id}',
                  AlarmDescription='memory usage over 50%',
                  OKActions=[
                      sns_arn
                  ],
                  AlarmActions=[
                      sns_arn
                  ],
                  MetricName='mem_used_percent',
                  Namespace='CWAgent',
                  Statistic='Average',
                  Dimensions=[
                      {
                          'Name':'InstanceId',
                          'Value': instance_id
                      }
                  ],
                  Period=60,
                  EvaluationPeriods=1,
                  DatapointsToAlarm=1,
                  Threshold=50,
                  ComparisonOperator='GreaterThanOrEqualToThreshold'
              )
      Environment:
        Variables:
          SNS_ARN: !Ref SNSEmailTopic
      FunctionName: create-alarm-lambda
      Handler: index.lambda_handler
      Role: !GetAtt LambdaIAMRole.Arn
      Runtime: python3.12
      Timeout: 5

  EventSource:
    Type: AWS::Lambda::EventSourceMapping
    Properties:
      EventSourceArn: !GetAtt SQSQueue.Arn
      FunctionName: create-alarm-lambda

# ------------------------------------------------------------#
# SNS
# ------------------------------------------------------------# 
  SNSEmailTopic:
    Type: AWS::SNS::Topic
    Properties:
      FifoTopic: false
      TopicName: email-sns-topic

  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !Ref Email
      Protocol: email
      TopicArn: !Ref SNSEmailTopic

  SNSSQSTopic:
    Type: AWS::SNS::Topic
    Properties:
      FifoTopic: false
      TopicName: sqs-sns-topic

  SQSSubscription:
    DependsOn: SQSQueue
    Type: AWS::SNS::Subscription
    Properties:
      Endpoint: !GetAtt SQSQueue.Arn
      Protocol: sqs
      TopicArn: !Ref SNSSQSTopic

  SNSSQSTopicPolicy:
    Type: AWS::SNS::TopicPolicy
    Properties:
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sns:Publish
            Resource: "*"
      Topics: 
        - !Ref SNSSQSTopic

# ------------------------------------------------------------#
# SQS
# ------------------------------------------------------------# 
  SQSQueue:
    Type: AWS::SQS::Queue
    Properties:
      DelaySeconds: 10
      QueueName: sqs-queue
      SqsManagedSseEnabled: true
      Tags:
        - Key: Name
          Value: sqs-queue
      VisibilityTimeout: 30

  SQSQueuePolicy:
    DependsOn: SNSSQSTopic
    Type: AWS::SQS::QueuePolicy
    Properties:
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: "Account used"
            Effect: "Allow"
            Principal:
              AWS: "*"
            Action:
              - "SQS:*"
            Resource: !GetAtt SQSQueue.Arn
          - Sid: "SNS used"
            Effect: "Allow"
            Principal:
              Service: "sns.amazonaws.com"
            Action:
              - "SQS:SendMessage"
            Resource: !GetAtt SQSQueue.Arn
            Condition:
              ArnLike:
                aws:SourceArn: !Ref SNSSQSTopic
      Queues:
        - !Ref SQSQueue
# ------------------------------------------------------------#
# EventBridge
# ------------------------------------------------------------# 
  EventBridge:
    Type: AWS::Events::Rule
    Properties: 
      EventPattern:
        source:
          - aws.autoscaling
        detail-type:
          - EC2 Instance Launch Successful
          - EC2 Instance Terminate Successful
        detail:
          AutoScalingGroupName:
            - wildcard: CodeDeploy_*
      Name: autoscaling-eventbridge
      State: ENABLED
      Targets: 
        - Arn: !Ref SNSSQSTopic
          Id: sns-topic

16~134行目でLambda関数に関連するリソースを作成しています。
Lambdaで使用するIAMロールにはAWSマネージドポリシーのAWSLambdaSQSQueueExecutionRoleとCloudWatch Alarmの作成、削除を行えるように「cloudwatch:PutMetricAlarm」、「cloudwatch:DeleteAlarms」を許可するインラインポリシーを設定しています。
139~177行目でSNSトピックを作成しています。
SNSトピックはCloudWatch Alarmで使用するものとSQSへメッセージを送るためのものを作成しています。
182~218行目でSQSキューを作成しています。
SQSキューではDelaySecondsでメッセージを処理できるようにするまで10秒間遅延を持たせています。
また、キューポリシーでSNSトピックからの「SQS:SendMessage」を許可しています。
222~238行目でEventBridgeを作成しています。
EventBridgeのルールは以下のようにすることで「CodeDeploy_」から始まるAutoScalingグループを対象とすることができます。

{
  "detail-type": ["EC2 Instance Launch Successful", "EC2 Instance Terminate Successful"],
  "source": ["aws.autoscaling"],
  "detail": {
    "AutoScalingGroupName": [{
      "wildcard": "CodeDeploy_*"
    }]
  }
}

デプロイは以下のAWS CLIコマンドを使用します。

aws cloudformation create-stack --stack-name CloudFormationスタック名 --template-body file://CloudFormationテンプレートファイル名 --parameters ParameterKey=Email,ParameterValue=メールアドレス --capabilities CAPABILITY_NAMED_IAM

動作確認

リソースの作成が完了したら実際にCodePipelineを動かしてデプロイを開始してみてください。
Lambda関数の実行が成功すると以下の画像のようにCloudWatch Alarmが作成されることが確認できます。

さいごに

AutoScalingで管理されたEC2に対してCloudWatch Alarmを設定するとアプリ起因による継続的なリソースの負荷増加など、インスタンスの入れ替えでは解決できない状況を検知するのに役立つことがあります。
状況に応じて上記のようなLambda関数を導入していただければと思います。