みなさんこんにちは。お元気ですか。自称プログラマの山口です。
暖かくなってきてビールがどんどんおいしくなってきましたね。
この記事では、CloudWatch Alarmの発報を条件にEC2内で任意のコマンドを実行する方法を説明します。
これにより、たとえば「CPU使用率が80%を超えた時、 $ ps aux --sort -%cpu の結果をファイルに保存する」といったことができます。
動機
CloudWatch Alarmの発報があった時、多くのサービスではAPMなどがインストールされており、原因の調査が簡単です。
一方、サービスの保守費用を減らす構成の場合、一台のEC2上にさまざまなアプリケーション(PHP, MySQL, nginx, その他バッチ処理)を詰め込んでおり、APMでは原因が特定できないことが多いです。
さらに、アラートの多くは深夜に発報することが多く(体感)、翌日になると調査が困難です。
そこで、アラートの発報を条件に ps コマンドなどを実行することで、その時のプロセスの一覧を取得し調査できるようにします。
設定の方法
1. 設定の概要
すでに以下のような構成があるとします。
- EC2: IAMロールとして、ポリシー `AmazonSSMManagedInstanceCore` が設定済みとします。
- CloudWatch Alarm: CPU使用率に関するアラートが設定済みとします。
以下のリソースを追加することで、CloudWatch Alarmをトリガーにコマンドを実行します。
- SNSトピック
- Lambda
- AWS Systems Manager Run Command
2. SNSトピックの作成とCloudWatch Alarmの設定
CloudWatch AlarmからLambda関数を呼び出すには、SNSトピックを経由する必要があります。
- SNSトピックを作成します。
- CloudWatch Alarmの設定で、アラーム状態になったときに作成したSNSトピックに通知するように設定します。
- 作成したSNSトピックをLambda関数のトリガーとして追加します。
3. Lambda関数の登録
今回はPythonで実装します。このコードはおそらく機能します。
import json
import boto3
ssm = boto3.client('ssm')
def lambda_handler(event, context):
try:
message = json.loads(event['Records'][0]['Sns']['Message'])
except (KeyError, json.JSONDecodeError) as e:
print(f"Error parsing SNS message: {e}")
return {"status": "error", "reason": "failed to parse message"}
alarm_name = message.get('AlarmName')
trigger = message.get('Trigger', {})
metric_name = trigger.get('MetricName')
namespace = trigger.get('Namespace')
# メトリック名を元にフィルタリングします。
# CPUUtilizationかつEC2以外の場合は何もしません。
if metric_name != "CPUUtilization" or namespace != "AWS/EC2":
print(f"Skipping alarm '{alarm_name}' (metric={metric_name}, namespace={namespace})")
return {"status": "skipped", "alarm": alarm_name}
instance_id = None
for d in trigger.get('Dimensions', []):
# DimensionsはName/Value(大文字)またはname/value(小文字)の形式に対応
if d.get('Name') == "InstanceId" or d.get('name') == "InstanceId":
instance_id = d.get('Value') or d.get('value')
break
if not instance_id:
print(f"No InstanceId found in alarm '{alarm_name}'")
return {"status": "skipped", "reason": "no instance id"}
print(f"Executing 'ps aux --sort -%cpu' on instance: {instance_id} (alarm: {alarm_name})")
try:
response = ssm.send_command(
InstanceIds=[instance_id],
DocumentName="AWS-RunShellScript",
Parameters={"commands": ["ps aux --sort -%cpu > /var/log/cpuusage_$(date +%Y%m%d-%H%M%S).log"]},
CloudWatchOutputConfig={"CloudWatchOutputEnabled": True}
)
print(f"Command sent successfully. CommandId: {response.get('Command', {}).get('CommandId')}")
return {"status": "sent", "instance": instance_id, "alarm": alarm_name, "command_id": response.get('Command', {}).get('CommandId')}
except Exception as e:
print(f"Error sending SSM command: {e}")
return {"status": "error", "reason": str(e), "instance": instance_id}
今回は結果を /var/log/ 以下に保存します。
注意: /var/log/ に書き込むには適切な権限が必要です。SSM Run Commandはデフォルトで root または ec2-user として実行されるため通常は問題ありませんが、環境によっては権限の調整が必要な場合があります。
4. LambdaにIAMロールを登録
上記のLambda関数に、以下のポリシーを許可するロールを登録します。
- ssm:SendCommand
IAMポリシーのJSON例は以下の通りです。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:SendCommand"
],
"Resource": "*"
}
]
}
5. 動作確認
動作確認のため、 stress コマンドを実行します。
# 無い場合はインストールします。
$ sudo dnf install stress
$ stress --cpu 2 --timeout 60
指定したディレクトリ /var/log/ 以下にログファイルが出力されていることを確認できたらOKです。
注意: stressコマンドを実行するとCPU負荷が上昇します。開発環境など障害が発生しても問題が無い環境で試してください。
さいごに
今回は、インフラ費用を抑えた構成のCPU使用率上昇時のログ調査を記載しました。
さくらのクラウド + Zabbix といった構成にすれば、さらにインフラ費用を抑えることができるので、次回はそれをテーマにしてみようと思います。