シャッフル主任の進捗報告

興味のあるものを作ります。進捗を不定期にご報告します。

そうだ、教祖になろう。出エジプト記 第3章6節 CloudWatch LogsをSNSで通知する

しろやぎさんからおてがみついた


第3章5節 LambdaのログをCloudWatch Logsに出力するでLambdaのログがCloudWatchに出力されているのを確認しました。
開発中はいちいち見るでもいいのですが、思いもよらぬところで出たエラーは見過ごしてしまいます。
ので、AWSに知らせてもらうことにしましょう。

CloudWatch Logsで特定のキーワードを含むログをLambdaに渡してSNS(Simple Notification Service)でメール通知します。

このような構成です。

f:id:chief-shuffle:20191213210107p:plain

ネット上にはCloudWatch AlarmからSNS経由でLambdaに連携する例もあったのですが、今回はCloudWatch Logsのサブスクリプションフィルターから直接Lambdaを起動しています。

くろやぎさんたらよまずにたべた


では、SNSから作っていきます。
「トピックの作成」をクリック。

f:id:chief-shuffle:20191213222214j:plain

トピック名を入力。
この下に連絡先であるメールアドレスをぶら下げます。

f:id:chief-shuffle:20191213221756j:plain

サブスクリプションの作成」をクリック。
「詳細」で以下を入力。

  • トピックARN:さっき作ったトピック
  • プロトコル:Eメール
  • エンドポイント:自分のメールアドレス

f:id:chief-shuffle:20191213221725j:plain

通知のサブスクリプション(購読)案内のメールが届きますので「Confirm subscription」で承諾します。

f:id:chief-shuffle:20191213221834p:plain

購読開始のメッセージが表示されます。

f:id:chief-shuffle:20191213221848p:plain

サブスクリプションが「確認済み」になります。
ちなみにエンドポイントのメールアドレスを入れ間違えると承諾も削除もできなくなり、3日後に復活…することなくAWSにより削除されます。

f:id:chief-shuffle:20191213222311j:plain

しかたがないのでおてがみかいた


次にSNSに通知を指示するLambda関数を作成します。

Cloud9で「AWS Resources」から「λ+」ボタンをクリック。

  • Lambda関数名:それっぽい名前
  • Lambdaアプリケーション名:serverside(rebirthと同じ)
  • ランタイム:Python3.6
  • Blueprint:empty-python
  • メモリ:128MB
  • Role:Automatically generate role

f:id:chief-shuffle:20191213233931j:plain

この通知用の関数でログイベントを整形してSNSに渡すのですが、引数のeventはどんな形なのでしょうか。
それを確認するためにこのlambda_function.pyを以下のように変えてみます。

def lambda_handler(event, context):
    print(event)
    return ''

通知元のLambda関数のエラーログを今作った通知用のLambdaに渡してみます。
CloudWatchの「ロググループ」で通知元のログを選択し、「AWS Lambdaへのストリーム」をクリック。

f:id:chief-shuffle:20191213234405j:plain

通知用のLambda関数を選択。

f:id:chief-shuffle:20191213234524j:plain

通常ログもすべて流し込むとLambda起動費用がかさんでしまいますので、エラーログだけを連携するようにフィルターを設定します。
「CRITICAL」か「ERROR」か「WARNING」を含む、はこのように書きます。

?CRITICAL ?ERROR ?WARNING

f:id:chief-shuffle:20191213234724j:plain

「パターンのテスト」でフィルターをテストします。
第3章5節 LambdaのログをCloudWatch Logsに出力するで発生させたログでテストすると、ERRORを1か所検知しました。

f:id:chief-shuffle:20191213234921j:plain

「ストリーミングの開始」をクリックすると、ロググループの「サブスクリプション」にLambda関数名が表示されます。

f:id:chief-shuffle:20191214070717j:plain

例によって通知元のLambdaの設定をエラーが起きるように変更してデプロイします。

f:id:chief-shuffle:20191213235308j:plain

CloudFront経由でアクセスしてみます。認証エラーが起きるので、通知用のLambdaが起動されてprint(event)により引数eventが出力されています。

f:id:chief-shuffle:20191213235545j:plain

なにやら長めの文字列が出力されているので、JSON形式のままコピーします。
Cloud9に.pyファイルを作って貼り付けます。

json = # JSONデータ

これはZLIB圧縮されてBase64文字列に変換されたログデータです。
これではよくわからないのでCloud9上で展開して中身を見てみましょう。

テストデータとして読めるように'シングルクォーテーション区切りを"ダブルクォーテーション区切りに変換します。

f:id:chief-shuffle:20191214000111j:plain

lambda_function.pyを以下のように変えます。
ZLIB&Base64をほどいてログイベントデータを出力します。

import zlib
import base64
import json


def lambda_handler(event, context):
    try:
        data = json.loads(
            zlib.decompress(base64.b64decode(event['awslogs']['data']),
                            16 + zlib.MAX_WBITS))
        log_events = json.loads(
            json.dumps(data["logEvents"], ensure_ascii=False))

        print(log_events)
    except Exception as e:
        return e

Lambda(local)で「Payload」にテストデータを貼り付けて実行すると、ログイベントの中身が見えました。

f:id:chief-shuffle:20191214000352j:plain

さらに深堀りしてみます。
lambda_function.pyを以下のように変えます。

import zlib
import base64
import json


def lambda_handler(event, context):
    try:
        data = json.loads(
            zlib.decompress(base64.b64decode(event['awslogs']['data']),
                            16 + zlib.MAX_WBITS))
        log_events = json.loads(
            json.dumps(data["logEvents"], ensure_ascii=False))

        log_messages = []
        for e in log_events:
            log_messages.append(e['message'])
        print(log_messages)

    except Exception as e:
        return e

やっと[ERROR]のエラー文言までたどり着きました。

それでは、これをSNSにPublishしたいと思います。
最終ソースはこんな感じ。

メール本文はログイベント内のmessageを連結した文字列。
メール題名は本文の最初の行ですが、長いので出力時刻とメッセージIDを除外、
接頭辞を付与した上で100文字以下というSNSという制限により、途中で切っています。

import os
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), '.'))

import zlib
import base64
import json
import re
import boto3
import logging
import settings

logger = logging.getLogger()
logger.setLevel(settings.LOGGER['level'])


def lambda_handler(event, context):
    try:
        data = json.loads(
            zlib.decompress(base64.b64decode(event['awslogs']['data']),
                            16 + zlib.MAX_WBITS))
        log_events = json.loads(
            json.dumps(data["logEvents"], ensure_ascii=False))

        log_messages = []
        for e in log_events:
            log_messages.append(e['message'])
        rows = re.split('\n', log_messages[0])
        words = rows[0].split()
        del (words[1:3])

        sns = boto3.client('sns')
        topic = settings.ALARM['topic']
        message = "\n".join(log_messages)
        subject = '{}{}'.format(settings.ALARM['subject-prefix'],
                                ' '.join(words))[:100]
        response = sns.publish(TopicArn=topic,
                               Message=message,
                               Subject=subject)
        return response

    except Exception as e:
        return e

ログレベルは同ディレクトリに作ったsettings.pyで指定しています。
トピックのARNがソースにべた書きですが、CloudFormationでSNSトピックごと作れるようになるまでは一旦このまま。

import logging
LOGGER = {'level': logging.INFO}
ALARM = {
    'topic': 'SNSトピックのARN',
    'subject-prefix': 'わかりやすい文言'
}

Lambda(local)で実行してみます。

f:id:chief-shuffle:20191214001024p:plain

メールでエラーログが届きました!

拝啓、LINEでよくないっすか


それでは、これをデプロイして実際に発生したエラーをリアルタイムに通知してみます。
エラーを起こしてから、CloudWatchで通知用のLambdaのログを見ると、

f:id:chief-shuffle:20191214001147j:plain

ん?

f:id:chief-shuffle:20191214001439j:plain

おっと。

cloud9-serverside-notify-XXXX is not authorized to perform: SNS:Publish on resource: arn:aws:sns:ap-northeast-1:9999:topic-notify-error

SNSにPublishする権限がないようです。
Cloud9はCloudFormationでデプロイしているため、ロールもCloudFormationで付与しないとデプロイのたびに元に戻ってしまいます。
今CloudFormationは手に負えない気がするので、なるべく簡易に権限を付与していきます。
まず、IAMでロールを作ります。

f:id:chief-shuffle:20191214001834j:plain

「このロールを使用するサービスを選択」で「Lambda」を選択。

f:id:chief-shuffle:20191214001933j:plain

ポリシーに以下を追加。
ガバガバですが、あとで見直しましょう。

  • CloudWatchFullAccess:ロググループ、ログストリーム、ログイベント作成用
  • AmazonSNSFullAccess:SNSのPublish用

f:id:chief-shuffle:20191214002110j:plain

Cloud9でLambdaアプリケーション直下にあるtemplate.yamlを開いて、通知用LambdaのProperties属性に以下を追加します。 さっきCloud9でLambda関数作るときに指定すればよかったですね。

Role: 'さっきつくったロールのARN'

ほんでもっかいデプロイ。
デプロイに時間がかかることがあるようで、ロールが反映されているかLambda画面で確認します。

f:id:chief-shuffle:20191214002510j:plain

もう一度エラーを起こしてみると、今度はちゃんと通知メールが届きました。

CloudFormationやロールが噛むと途端にわからなくなって困ります。
いずれきれいに整理したいと思います。