そうだ、教祖になろう。出エジプト記 第3章6節 CloudWatch LogsをSNSで通知する
しろやぎさんからおてがみついた
第3章5節 LambdaのログをCloudWatch Logsに出力するでLambdaのログがCloudWatchに出力されているのを確認しました。
開発中はいちいち見るでもいいのですが、思いもよらぬところで出たエラーは見過ごしてしまいます。
ので、AWSに知らせてもらうことにしましょう。
CloudWatch Logsで特定のキーワードを含むログをLambdaに渡してSNS(Simple Notification Service)でメール通知します。
このような構成です。
ネット上にはCloudWatch AlarmからSNS経由でLambdaに連携する例もあったのですが、今回はCloudWatch Logsのサブスクリプションフィルターから直接Lambdaを起動しています。
くろやぎさんたらよまずにたべた
では、SNSから作っていきます。
「トピックの作成」をクリック。
トピック名を入力。
この下に連絡先であるメールアドレスをぶら下げます。
「サブスクリプションの作成」をクリック。
「詳細」で以下を入力。
- トピックARN:さっき作ったトピック
- プロトコル:Eメール
- エンドポイント:自分のメールアドレス
通知のサブスクリプション(購読)案内のメールが届きますので「Confirm subscription」で承諾します。
購読開始のメッセージが表示されます。
サブスクリプションが「確認済み」になります。
ちなみにエンドポイントのメールアドレスを入れ間違えると承諾も削除もできなくなり、3日後に復活…することなくAWSにより削除されます。
しかたがないのでおてがみかいた
次にSNSに通知を指示するLambda関数を作成します。
Cloud9で「AWS Resources」から「λ+」ボタンをクリック。
- Lambda関数名:それっぽい名前
- Lambdaアプリケーション名:serverside(rebirthと同じ)
- ランタイム:Python3.6
- Blueprint:empty-python
- メモリ:128MB
- Role:Automatically generate role
この通知用の関数でログイベントを整形してSNSに渡すのですが、引数のevent
はどんな形なのでしょうか。
それを確認するためにこのlambda_function.py
を以下のように変えてみます。
def lambda_handler(event, context): print(event) return ''
通知元のLambda関数のエラーログを今作った通知用のLambdaに渡してみます。
CloudWatchの「ロググループ」で通知元のログを選択し、「AWS Lambdaへのストリーム」をクリック。
通知用のLambda関数を選択。
通常ログもすべて流し込むとLambda起動費用がかさんでしまいますので、エラーログだけを連携するようにフィルターを設定します。
「CRITICAL」か「ERROR」か「WARNING」を含む、はこのように書きます。
?CRITICAL ?ERROR ?WARNING
「パターンのテスト」でフィルターをテストします。
第3章5節 LambdaのログをCloudWatch Logsに出力するで発生させたログでテストすると、ERRORを1か所検知しました。
「ストリーミングの開始」をクリックすると、ロググループの「サブスクリプション」にLambda関数名が表示されます。
例によって通知元のLambdaの設定をエラーが起きるように変更してデプロイします。
CloudFront経由でアクセスしてみます。認証エラーが起きるので、通知用のLambdaが起動されてprint(event)
により引数event
が出力されています。
なにやら長めの文字列が出力されているので、JSON形式のままコピーします。
Cloud9に.py
ファイルを作って貼り付けます。
json = # JSONデータ
これはZLIB圧縮されてBase64文字列に変換されたログデータです。
これではよくわからないのでCloud9上で展開して中身を見てみましょう。
テストデータとして読めるように'
シングルクォーテーション区切りを"
ダブルクォーテーション区切りに変換します。
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」にテストデータを貼り付けて実行すると、ログイベントの中身が見えました。
さらに深堀りしてみます。
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)で実行してみます。
メールでエラーログが届きました!
拝啓、LINEでよくないっすか
それでは、これをデプロイして実際に発生したエラーをリアルタイムに通知してみます。
エラーを起こしてから、CloudWatchで通知用のLambdaのログを見ると、
ん?
おっと。
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でロールを作ります。
「このロールを使用するサービスを選択」で「Lambda」を選択。
ポリシーに以下を追加。
ガバガバですが、あとで見直しましょう。
- CloudWatchFullAccess:ロググループ、ログストリーム、ログイベント作成用
- AmazonSNSFullAccess:SNSのPublish用
Cloud9でLambdaアプリケーション直下にあるtemplate.yaml
を開いて、通知用LambdaのProperties
属性に以下を追加します。
さっきCloud9でLambda関数作るときに指定すればよかったですね。
Role: 'さっきつくったロールのARN'
ほんでもっかいデプロイ。
デプロイに時間がかかることがあるようで、ロールが反映されているかLambda画面で確認します。
もう一度エラーを起こしてみると、今度はちゃんと通知メールが届きました。
CloudFormationやロールが噛むと途端にわからなくなって困ります。
いずれきれいに整理したいと思います。