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へのエクスポートまでの連携方法でした。
いやぁ、ほんと便利ですね。
- 次の記事