はじめに
こんにちは!
BFT名古屋支店・インフラ女子(?)のやまぐちです。
前回の投稿でAWSマネジメントコンソールにログインしたらSlackへ通知する仕組みの実装をしましたが、Slackに通知される内容はアラーム内容なので、「ログインした」ということはわかるけど「誰が」ということが分からずイマイチな状況でした。
CloudTrail経由でのログを通知内容に含めるためには、① Lambdaでログ内容をもう一度フィルタして抽出するか、② サブスクリプションフィルタを使用するかどちらか、みたいなので、シンプルな構成の② サブスクリプションフィルタを使用するで実装してみました。
前回の構成からの変更
CloudWatchでアラームを作成し、それをトリガーとしてSNS、Lambdaを実行していましたが、サブスクリプションフィルタを使用すればこのSNS部分が不要になるのでアラームもいりません。
Lambda関数の作成
CloudWatch Logsでサブスクリプションフィルタを作成し、特定の条件がログに出力されたらLambdaを実行するので、先にLambda関数を作成します。
コードは前回のものと以下の記事を参考に作成しました。
CloudWatch Logsの特定文字を検知してログ内容を通知するLambda Function | DevelopersIO
import boto3 import json import logging import os import base64 import gzip from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError HOOK_URL = os.environ['IncomingWebhookUrl'] SLACK_CHANNEL = os.environ['SlackChannel'] logger = logging.getLogger() logger.setLevel(logging.INFO) def lambda_handler(event, context): # CloudWatchLogsからのデータはbase64エンコードされているのでデコード decoded_data = base64.b64decode(event['awslogs']['data']) # バイナリに圧縮されているため展開 json_data = json.loads(gzip.decompress(decoded_data)) # CloudWatch Logsに複合化&解凍したログを出力 logger.info("Event: " + json.dumps(json_data)) message = json_data['logEvents'][0]['message'] # CloudWatch Logsにmessageの内容のみをログを出力 logger.info("Message: " + str(message)) json_message = json.loads(message) login_name = json_message['userIdentity']['userName'] # Slackへのメッセージを作成 slack_message = { 'channel': SLACK_CHANNEL, 'text': "%s さんがAWSへログインしました!" % (login_name) } req = Request(HOOK_URL, json.dumps(slack_message).encode('utf-8')) try: response = urlopen(req) response.read() logger.info("Message posted to %s", slack_message['channel']) except HTTPError as e: logger.error("Request failed: %d %s", e.code, e.reason) except URLError as e: logger.error("Server connection failed: %s", e.reason)
Lambdaのポイント
全部のログを転送する、という例はあるのですが、今回はログの一部(ログインしたIAMユーザ名)を抽出したいだけです。python全然わからないのでエラーの対処に苦労しました。。。
エラー内容: [ERROR] TypeError: string indices must be integers
出力した内容は「message」に格納されています。
[INFO] 2021-06-14T07:05:02.513Z ef4dec33-b52c-4aeb-9762-37edf73f564e Message: { "eventVersion": "1.08", "userIdentity": { "type": "IAMUser", "principalId": "XXXXXX", "arn": "arn:aws:iam::XXXXXX", "accountId": "XXXXXX", "userName": "a-yamaguchi" # 以下略 } }
ここでmessageの中にあるuserIdentityのuserNameから値を取り出したいのですがString型なので整数で返せ、ということらしいです。
《修正前》
当初userName(かその前のuserIdentity)までmessageに入れるようにしていました。
message = json_data['logEvents'][0]['message']['userIdentity']['userName']
《修正後》
ログの内容であるmessegeがstr型になっているので、一度そこまでを変数messageに入れて
message = json_data['logEvents'][0]['message']
そのまま 「変数 = message['userIdentity']['userName']」としようとすると同じように怒られるため、json.loadsでdict型に変えて
json_message = json.loads(message)
そこからuserNameを取り出します。
login_name = json_message['userIdentity']['userName']
サブスクリプションフィルタの作成
サブスクリプションフィルタは1ロググループごとに二つ設定できます。
設定は難しくありません。前回で使用したフィルタ条件を使い、作成したLambda関数を紐づけます。
ログインしてみた
CloudWatch LogsからLambdaへ送信されるログは圧縮、Base64で暗号化されています。そのためLambda上でテストデータを送信できないので実際にサインイン&サインアウトして確認しました。
Slackに通知されたメッセージです。
終わりに
pythonわからないとこういうところで躓くんですよね…
ちなみに前回と同様タイムラグはあります。CloudWatch logsに連携されるまでの時間はかかるのでリアルタイムではないですが、誰がログインしたという情報はわかりやすくなりました。やった~。