CloudWatch Logs + Lambda + SNSでEC2のログ監視を実装してみよう
目次
こんにちは。スカイアーチHRソリューションズのきむです。
サーバを運用するにあたってログ監視の仕組みは必須ですよね。
クラウド環境におけるログ監視の選択肢は多岐にわたりますが、今回はAWSのみで完結するログ監視の設定について簡単にご紹介させていただきます。
今回作成するアーキテクチャは以下のようになります。
はじめに
この記事では以下の手順を実施します。
- EC2インスタンスにCloudWatch Agentのインストール
- CloudWatch Agentのリソース定義ファイル設定
- 特定文字列監視に必要なリソースのSAMデプロイ。
対象となるEC2インスタンス(Amazon Linux2)は既ににデプロイしてあるものを使用します。
また、手順の中でEC2インスタンスやLambdaのIAMロールにフルアクセス権限を付与していますが、実際の運用では適切な権限設定をお願いします。
EC2の設定
対象となるEC2インスタンスからCloudWatch Logsに対して、ログを送信できるように設定していきます。
EC2インスタンスのIAMロールに以下のマネージドポリシーをアタッチします。
・CloudWatchFullAccess
EC2インスタンスに接続して、CloudWatch Agentのインストールを行います。
$ sudo yum -y install amazon-cloudwatch-agent
CloudWatch Agentのリソース定義ファイルを作成します。
今回は「amazon-cloudwatch-agent-config-wizard」を利用して対話型で設定していきます。
$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-config-wizard
================================================================
= Welcome to the Amazon CloudWatch Agent Configuration Manager =
= =
= CloudWatch Agent allows you to collect metrics and logs from =
= your host and send them to CloudWatch. Additional CloudWatch =
= charges may apply. =
================================================================
On which OS are you planning to use the agent?
1. linux
2. windows
3. darwin
default choice: [1]:
1
Trying to fetch the default region based on ec2 metadata...
Are you using EC2 or On-Premises hosts?
1. EC2
2. On-Premises
default choice: [1]:
1
Which user are you planning to run the agent?
1. root
2. cwagent
3. others
default choice: [1]:
1
Do you want to turn on StatsD daemon?
1. yes
2. no
default choice: [1]:
2
Do you want to monitor metrics from CollectD? WARNING: CollectD must be installed or the Agent will fail to start
1. yes
2. no
default choice: [1]:
2
Do you want to monitor any host metrics? e.g. CPU, memory, etc.
1. yes
2. no
default choice: [1]:
1
Do you want to monitor cpu metrics per core?
1. yes
2. no
default choice: [1]:
1
Do you want to add ec2 dimensions (ImageId, InstanceId, InstanceType, AutoScalingGroupName) into all of your metrics if the info is available?
1. yes
2. no
default choice: [1]:
1
Do you want to aggregate ec2 dimensions (InstanceId)?
1. yes
2. no
default choice: [1]:
1
Would you like to collect your metrics at high resolution (sub-minute resolution)? This enables sub-minute resolution for all metrics, but you can customize for specific metrics in the output json file.
1. 1s
2. 10s
3. 30s
4. 60s
default choice: [4]:
4
Which default metrics config do you want?
1. Basic
2. Standard
3. Advanced
4. None
default choice: [1]:
1
Current config as follows:
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"metrics": {
"aggregation_dimensions": [
[
"InstanceId"
]
],
"append_dimensions": {
"AutoScalingGroupName": "${aws:AutoScalingGroupName}",
"ImageId": "${aws:ImageId}",
"InstanceId": "${aws:InstanceId}",
"InstanceType": "${aws:InstanceType}"
},
"metrics_collected": {
"disk": {
"measurement": [
"used_percent"
],
"metrics_collection_interval": 60,
"resources": [
"*"
]
},
"mem": {
"measurement": [
"mem_used_percent"
],
"metrics_collection_interval": 60
}
}
}
}
Are you satisfied with the above config? Note: it can be manually customized after the wizard completes to add additional items.
1. yes
2. no
default choice: [1]:
1
Do you have any existing CloudWatch Log Agent (http://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AgentReference.html) configuration file to import for migration?
1. yes
2. no
default choice: [2]:
2
Do you want to monitor any log files?
1. yes
2. no
default choice: [1]:
1
Log file path:
/var/log/messages
Log group name:
default choice: [messages]
messages
Log stream name:
default choice: [{instance_id}]
{instance_id}
Log Group Retention in days
1. -1
2. 1
3. 3
4. 5
5. 7
6. 14
7. 30
8. 60
9. 90
10. 120
11. 150
12. 180
13. 365
14. 400
15. 545
16. 731
17. 1827
18. 2192
19. 2557
20. 2922
21. 3288
22. 3653
default choice: [1]:
9
Do you want to specify any additional log files to monitor?
1. yes
2. no
default choice: [1]:
2
Saved config file to /opt/aws/amazon-cloudwatch-agent/bin/config.json successfully.
Current config as follows:
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/messages",
"log_group_name": "messages",
"log_stream_name": "{instance_id}",
"retention_in_days": 90
}
]
}
}
},
"metrics": {
"aggregation_dimensions": [
[
"InstanceId"
]
],
"append_dimensions": {
"AutoScalingGroupName": "${aws:AutoScalingGroupName}",
"ImageId": "${aws:ImageId}",
"InstanceId": "${aws:InstanceId}",
"InstanceType": "${aws:InstanceType}"
},
"metrics_collected": {
"disk": {
"measurement": [
"used_percent"
],
"metrics_collection_interval": 60,
"resources": [
"*"
]
},
"mem": {
"measurement": [
"mem_used_percent"
],
"metrics_collection_interval": 60
}
}
}
}
Please check the above content of the config.
The config file is also located at /opt/aws/amazon-cloudwatch-agent/bin/config.json.
Edit it manually if needed.
Do you want to store the config in the SSM parameter store?
1. yes
2. no
default choice: [1]:
2
Program exits now.
作成されたファイルの中身を確認してみましょう。
$ cat /opt/aws/amazon-cloudwatch-agent/bin/config.json
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root"
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"file_path": "/var/log/messages",
"log_group_name": "messages",
"log_stream_name": "{instance_id}",
"retention_in_days": 90
}
]
}
}
},
"metrics": {
"aggregation_dimensions": [
[
"InstanceId"
]
],
"append_dimensions": {
"AutoScalingGroupName": "${aws:AutoScalingGroupName}",
"ImageId": "${aws:ImageId}",
"InstanceId": "${aws:InstanceId}",
"InstanceType": "${aws:InstanceType}"
},
"metrics_collected": {
"disk": {
"measurement": [
"used_percent"
],
"metrics_collection_interval": 60,
"resources": [
"*"
]
},
"mem": {
"measurement": [
"mem_used_percent"
],
"metrics_collection_interval": 60
}
}
}
「logs」セクションにCloudWatch Logsに送信するログ定義が記述されています。
フィールド | 概要 |
---|---|
file_path | 監視対象となるログのフルパスを指定します。 今回はテストがしやすい/var/log/messagesを指定しています。 |
log_group_name | ログの送信先となるCloudWatch Logsのロググループ名を指定します。 存在しないロググループ名を指定した場合は、新規で作成されます。 |
log_stream_name | 送信先のロググループで表示されるログストリーム名を指定します。 今回はインスタンスIDをログストリーム名としました。 |
retention_in_days | ロググループがログを保持期間を指定します。今回は90日としました。 ” log_group_name “フィールドで既に存在しているロググループを指定し、このフィールドを指定すると保存期間が上書きされます。 |
その他のセクションの記述内容についてはこちらの公式ドキュメントをご参照ください。
定義ファイルの作成が完了したら、設定を反映させましょう。
$ sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a fetch-config -m ec2 -c file:/opt/aws/amazon-cloudwatch-agent/bin/config.json -s
****** processing amazon-cloudwatch-agent ******
I! Trying to detect region from ec2 D! [EC2] Found active network interface Successfully fetched the config and saved in /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_config.json.tmp
Start configuration validation...
2023/03/18 17:19:28 Reading json config file path: /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.d/file_config.json.tmp ...
2023/03/18 17:19:28 I! Valid Json input schema.
I! Detecting run_as_user...
I! Trying to detect region from ec2
D! [EC2] Found active network interface
No csm configuration found.
Configuration validation first phase succeeded
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent -schematest -config /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.toml
Configuration validation second phase succeeded
Configuration validation succeeded
amazon-cloudwatch-agent has already been stopped
Created symlink /etc/systemd/system/multi-user.target.wants/amazon-cloudwatch-agent.service → /etc/systemd/system/amazon-cloudwatch-agent.service.
リソース定義ファイルで設定した内容通りにログが送信されているかをCloudWatchLogsから確認してみましょう。
これでEC2インスタンス側の設定は完了となります。
特定文字列の監視
続いては特定文字列を検知した際にメールで通知が飛ぶように設定していきます。
冒頭でも説明したように、以下のリソースをSAMで作成していきます。
- SNSトピック
- Lambda用IAMロール
- Lambda関数
- Lambdのリソースベースポリシー
- サブスクリプションフィルター
今回SAMでデプロイするYAMLは以下の通りです。
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
SNSTopic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: "通知を飛ばしたいEメールアドレス"
Protocol: email
TopicName: LogsTopic
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/AmazonSNSFullAccess
RoleName: LambdaRole
Lambda:
Type: AWS::Serverless::Function
Properties:
FunctionName: SubscriptionLambda
Description: "サブスクリプションフィルター用Lambda"
CodeUri: ./functions
Handler: app.lambda_handler
Runtime: python3.9
MemorySize: 256
Role: !GetAtt LambdaIamRole.Arn
Timeout: 300
Architectures:
- x86_64
Environment:
Variables:
SNSTopic: !GetAtt SNSTopic.TopicArn
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt Lambda.Arn
Principal: !Sub "logs.${AWS::Region}.amazonaws.com"
SourceArn: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*"
SubscriptionFilter:
Type: AWS::Logs::SubscriptionFilter
Properties:
DestinationArn: !GetAtt Lambda.Arn
FilterName: messages_filter
FilterPattern: '[(msg = "*error*" || msg = "*fatal*" || msg = "*ERROR*" || msg = "*FATAL*")]'
LogGroupName: messages
リソース名 | 概要 |
---|---|
SNSTopic | 今回使用するSNSトピックです。 Subcriptionフィールドで通知先のEメールアドレスを指定します。 |
LambdaIamRole | LambdaからSNSトピックにメッセージをパブリッシュする必要があるため、マネージドポリシーである「AmazonSNSFullAccess」をアタッチしています。 |
Lambda | 今回のランタイムはPython3.9です。 ソースコードは「./functions/app.py」に配置しています。 後述するLambdaのソースコード内で、SNSトピックのArnを環境変数で取得するため、Environmentフィールドで環境変数を設定しています。 |
LambdaPermission | CloudWatch LogsからLambdaを実行できるように、LambdaのリソースベースポリシーでCloudWatch Logsからの呼び出しを許可しています。 |
SubscriptionFilter | 今回のフィルタリング対象は[error, fatal, ERRO, FATAL]です。 サブスクリプションフィルターは1つのロググループに2つまでしか設定できないため、OR条件で複数のキーワードを設定します。 |
今回使うLambdaのソースコードはこちら。
import base64
import datetime
import json
import os
import zlib
import boto3
sns = boto3.client("sns")
def lambda_handler(event, context):
data = zlib.decompress(base64.b64decode(event["awslogs"]["data"]), 16 + zlib.MAX_WBITS)
data_json = json.loads(data)
for log in data_json["logEvents"]:
log_json = json.loads(json.dumps(log, ensure_ascii=False))
if data_json["logGroup"]:
date = datetime.datetime.fromtimestamp(int(str(log_json["timestamp"])[:10])) + datetime.timedelta(hours=9)
sns_body = {}
sns_body["default"] = ""
sns_body["default"] += f'ロググループ名:{data_json["logGroup"]} \n'
sns_body["default"] += f'ログストリーム名:{data_json["logStream"]} \n'
sns_body["default"] += f'発生時間:{date.strftime("%Y-%m-%d %H:%M:%S")}\n'
sns_body["default"] += f'検知メッセージ:{log_json["message"]}\n'
response = sns.publish(
TopicArn=os.environ.get("SNSTopic"),
Message=json.dumps(sns_body, ensure_ascii=False),
Subject=f'{data_json["logStream"]}でログ監視エラーが発生しました',
MessageStructure="json",
)
return
メール本文は自由にカスタマイズできます。今回は以下の内容を表示させます。
- 件名:インスタンスID(ログストリーム名)
- 本文
- ロググループ名
- ログストリーム名
- 検知時間(JST)
- 検知メッセージ
それではSAMでデプロイしていきましょう。
$ sam build
---省略---
$ sam deploy
---省略---
デプロイが確認出来たら、メールの通知テストを行いましょう。
今回はEC2インスタンスに接続して、「logger」コマンドでフィルタリング対象となる文字列を「messages」に出力させます。
$ logger error
設定がうまくいっていると以下のようなメールが届くかと思います。
他のメッセージでもメールが届くか試してみましょう。
おわりに
お疲れさまでした。
AWSサービスだけで完結するので思ったより簡単にログ監視の仕組みを作成できたのではないでしょうか。
懸念点として、エラーメッセージ1件につきLambdaが起動するので、短時間で大量にエラーが発生してしまうとLambdaの実行数が膨れ上がる恐れがあります。
そういったシチュエーションが想定されるログはフィルタリングするキーワードの選定に気を付けましょう。
この記事が少しでも皆様のご参考になれば幸いです。