awslogsでEC2のログをCloudWatch連携する方法
こんにちは、エンジニアのよこやまです。
EC2インスタンス内のログをawslogsでCloudWatch連携する方法について紹介します。
<やりたいこと>
- ログをCloudWatchに送信する
- CloudWatchでログを監視してSlackやメールでアラートする
- 日々溜まっていくCloudWatchログを定期的にS3にエクスポートする
ログをCloudWatchに送信する
Amazon LinuxのログのCloudWatch連携は、「awslogs」を使うと簡単に実現できます。
以下のサイトを参考に、非常に簡単に主要なログをCloudWatch連携できました。
https://qiita.com/zaburo/items/57bf357065b7391e1a9d
まず、EC2にCloudWatchログ出力可能なロールを関連付けします。リファレンスを参考に、必要なポリシーを設定します。
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream", | |
"logs:PutLogEvents", | |
"logs:DescribeLogStreams" | |
], | |
"Resource": [ | |
"arn:aws:logs:*:*:*" | |
] | |
} | |
] | |
} |
ロールの関連付けをしたあとは、awslogsのインストール、confファイル修正、ログ出力の定義を設定してサービス起動して終了です。設定手順は以下の通りです。
$ sudo yum install awslogs | |
$ cd /etc/awslogs/ | |
$ sudo vi awscli.conf | |
$ sudo vi awslogs.conf | |
$ sudo service awslogs start | |
$ sudo chkconfig awslogs on |
※ 3行目の「awscli.conf」は、"region"を
に変更します。
※ 4行目の「awslogs.conf」は、以下のように取得したいログファイルの定義を追加します。
※awslogs.confの"file"にはログファイルのパスを記述します。
※awslogs.confの"datetime_format"は、対象ログから時刻を抽出する際に設定します。
日付書式設定については、リファレンスに記載されています。
正しく設定ができていれば、CloudWatch上にログが出力されます。
CloudWatchでログを監視してSlackやメールでアラートする
CloudWatchログでは、指定されたものに一致する語句や値をログから検索できる「メトリクスフィルタ」と、フィルタ結果を基にアラート通知させる「アラーム」を設定することができます。
フィルタ設定内容はログによって様々なので、本記事では具体的な手順は記載しませんが、参考にさせていただいた記事を載せておきます。
・Nginxの500系エラー検知:https://remotestance.com/blog/2433/
・フィルタとパターン構文:https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/logs/FilterAndPatternSyntax.html
アラーム設定時に"アクション"で通知先のSNSトピックを設定することで、アラートのメール通知ができます。
通知は、アラームの状態が変化する毎に通知されます。「警告(Alert)」「OK(OK)」それぞれ通知を設定しておくと、警告〜警告の解消が把握できるので便利です。
アラートのSlack通知は、Lambdaに標準で提供されているSlack通知用のBlueprint(cloudwatch-alarm-to-slack-python3)を使うと簡単に実現できます。
・参考:http://blog.serverworks.co.jp/tech/2016/02/16/lambda-cloudwatch-alarm-to-slack/
このような感じでアラートがSlack通知されます(文面はBlueprintの"slack_message"をカスタマイズします)。
CloudWatchログを定期的にS3にエクスポートする
Amazon LinuxのログをCloudWatchに出力して監視もできましたが、このままではCloudWatch上に延々とログが蓄積されてしまうので、自動でS3にエクスポートしてCloudWatchログは一定期間のみ保持するようにします。
S3へのエクスポートはLambdaを使って自動化できますが、CloudWatchのCreateExportTaskは複数同時に実行できないため、1つエクスポートが終わってから次...と実行していく必要があります。
そのため、今回はStepFunctionsとLambdaを組み合わせて実現します。
まずは、ログをエクスポートするS3バケットを作成します。
また、作成したS3バケットに以下のようにバケットボリシーを設定しておきます。
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Principal": { | |
"Service": "logs.ap-northeast-1.amazonaws.com" | |
}, | |
"Action": "s3:GetBucketAcl", | |
"Resource": "arn:aws:s3:::(バケット名)" | |
}, | |
{ | |
"Effect": "Allow", | |
"Principal": { | |
"Service": "logs.ap-northeast-1.amazonaws.com" | |
}, | |
"Action": "s3:PutObject", | |
"Resource": "arn:aws:s3:::(バケット名)/*", | |
"Condition": { | |
"StringEquals": { | |
"s3:x-amz-acl": "bucket-owner-full-control" | |
} | |
} | |
} | |
] | |
} |
次に、作成したS3バケットにCloudWatchログをエクスポートするLambda関数を作成します。
Lambdaは以下の通り作成しました(python3.6)。
import logging | |
import boto3 | |
from datetime import datetime | |
from datetime import timedelta | |
from datetime import timezone | |
from dateutil.parser import parse | |
import time | |
import os | |
def _is_executing_export_tasks(): | |
''' | |
CloudWatch LogsのS3エクスポートタスク実行状態を判定する. | |
''' | |
client = boto3.client('logs') | |
for status in ['PENDING', 'PENDING_CANCEL', 'RUNNING']: | |
response = client.describe_export_tasks( | |
limit = 50, | |
statusCode=status | |
) | |
if 'exportTasks' in response and len(response['exportTasks']) > 0: | |
return True | |
return False | |
def _get_target_date(event): | |
''' | |
CloudWatch Eventsの(実行日時 - 1)日をエクスポート対象にする | |
''' | |
target = None | |
JST = timezone(timedelta(hours=+9), 'JST') | |
utc_dt = datetime.strptime(event['time'], '%Y-%m-%dT%H:%M:%SZ') | |
jst_time = utc_dt.astimezone(JST) | |
target = jst_time - timedelta(days=1) | |
t = target.replace(hour=0, minute=0, second=0, microsecond=0) | |
target_date = t.strftime('%Y%m%d') | |
from_time = int(t.timestamp() * 1000) | |
to_time = int((t + timedelta(days=1) - timedelta(milliseconds=1)).timestamp() * 1000) | |
return from_time, to_time, target_date | |
def _get_log_group(next_token): | |
client = boto3.client('logs') | |
if next_token is not None and next_token != '': | |
response = client.describe_log_groups( | |
limit = 50, | |
nextToken = next_token | |
) | |
else: | |
# nextTokenは空文字を受け付けない | |
response = client.describe_log_groups(limit = 50) | |
if 'logGroups' in response: | |
yield from response['logGroups'] | |
# ロググループが多くて50件(最大)を超えるようなら再帰呼出し | |
if 'nextToken' in response: | |
yield from _get_log_group(next_token = response['nextToken']) | |
def _is_bucket_object_exists(bucket_name, bucket_dir): | |
client = boto3.client('s3') | |
response = client.list_objects_v2( | |
Bucket = bucket_name, | |
Prefix = (bucket_dir) | |
) | |
return 'Contents' in response and len(response['Contents']) > 0 | |
def _export_logs_to_s3(bucket_name, bucket_dir, from_time, to_time, log_group_name): | |
client = boto3.client('logs') | |
response = client.create_export_task( | |
taskName = bucket_dir, | |
logGroupName = log_group_name, | |
fromTime = from_time, | |
to = to_time, | |
destination = bucket_name, | |
destinationPrefix = bucket_dir | |
) | |
def lambda_handler(event, context): | |
bucket_name = os.environ['BUCKET_NAME'] | |
from_time, to_time, target_date = _get_target_date(event=event) | |
# export_taskは複数同時実行できないため、他のタスクが実行中の場合はreturnする | |
if _is_executing_export_tasks(): | |
return { | |
"status": "running", | |
"time": event['time'] | |
} | |
for log_group in _get_log_group(next_token=None): | |
bucket_dir_prefix = target_date + '/' | |
bucket_dir = bucket_dir_prefix + log_group['logGroupName'] | |
if log_group['logGroupName'].find('/') == 0: | |
bucket_dir = bucket_dir_prefix + log_group['logGroupName'][1:] | |
if _is_bucket_object_exists(bucket_name = bucket_name, bucket_dir = bucket_dir): | |
continue | |
# ログエクスポート処理を実行 | |
_export_logs_to_s3( | |
bucket_name = bucket_name, | |
log_group_name = log_group['logGroupName'], | |
from_time= from_time, | |
to_time = to_time, | |
bucket_dir = bucket_dir | |
) | |
return { | |
"status": "running", | |
"time": event['time'] | |
} | |
return { | |
"status": "completed", | |
"time": event['time'] | |
} |
・参考:https://www.soudegesu.com/aws/export-cloudwatchlogs-to-s3/
なお、Lambdaには以下のIAMロールのポリシーを設定しました。
{ | |
"Version": "2012-10-17", | |
"Statement": [ | |
{ | |
"Effect": "Allow", | |
"Action": [ | |
"logs:CreateLogGroup", | |
"logs:CreateLogStream", | |
"logs:PutLogEvents", | |
"logs:DescribeLogStreams" | |
], | |
"Resource": [ | |
"arn:aws:logs:*:*:*" | |
] | |
} | |
] | |
} |
次に、ロググループ毎に上記Lambda関数を実行するStepFunctionsステートマシンを定義します。
ステートマシンでは、ロググループ毎にcreate export taskを行い、タスクが処理中の場合は数秒待ってリトライを行います。
こちらは、上記参考サイトの手順そのままに実装して実現できました。
あとは、CloudWatchの「ルール>ルールの作成」で、
- イベントソース:任意のパターン、スケジュール
- ターゲット:Step Functions ステートマシン
- ステートマシン:上記で作成したステートマシン
を設定すると、自動でCloudWatchログが定期的にS3エクスポートされます。
また、S3に過去ログを退避させることで、CloudWatchのログ保持期間やS3のライフサイクルルールを設定して、
- CloudWatchログは1週間保持
- S3にはログを3ヶ月保持
といった管理ができます。
以上が、CloudWatchへのログ出力からログ監視、S3へのエクスポートまでの連携方法でした。
いやぁ、ほんと便利ですね。
- 次の記事