BFT名古屋 TECH BLOG

日々の業務で得た知識を所属するエンジニアたちがアウトプットしていきます。

【CloudWatch・サブスクリプションフィルタ・Slack】出力されるログの一部を抽出してSlackにわかりやすいメッセージ出力してみた

はじめに

こんにちは!
BFT名古屋支店・インフラ女子(?)のやまぐちです。

前回の投稿でAWSマネジメントコンソールにログインしたらSlackへ通知する仕組みの実装をしましたが、Slackに通知される内容はアラーム内容なので、「ログインした」ということはわかるけど「誰が」ということが分からずイマイチな状況でした。

f:id:bftnagoya:20210614160720p:plain:w450

CloudTrail経由でのログを通知内容に含めるためには、① Lambdaでログ内容をもう一度フィルタして抽出するか、② サブスクリプションフィルタを使用するかどちらか、みたいなので、シンプルな構成のサブスクリプションフィルタを使用するで実装してみました。

前回の構成からの変更

f:id:bftnagoya:20210614185611p:plain:w550
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関数を紐づけます。
f:id:bftnagoya:20210614164852p:plain:w550

ログインしてみた

CloudWatch LogsからLambdaへ送信されるログは圧縮、Base64で暗号化されています。そのためLambda上でテストデータを送信できないので実際にサインイン&サインアウトして確認しました。

Slackに通知されたメッセージです。
f:id:bftnagoya:20210614184349p:plain:w550

終わりに

pythonわからないとこういうところで躓くんですよね…
ちなみに前回と同様タイムラグはあります。CloudWatch logsに連携されるまでの時間はかかるのでリアルタイムではないですが、誰がログインしたという情報はわかりやすくなりました。やった~。