AutoScalingで管理されているEC2に対してCloudWatch Alarmを設定するLambdaを作成したのでブログに残します。
やること
簡易的ですが構成図を作成しました。
AWSサービスとしてはAmazon EventBridge、Amazon SNS、Amazon SQS、AWS 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関数を導入していただければと思います。