Lambdaでも、例外エラー(想定外のエラー)が発生した場合に、通知(できればスタックトレースも)を受け取って、初動を早くしたいですね。
どんな方法で実現するのが良いか検討してみました。

先に結論を記載しちゃいます。
非同期(S3トリガーやEventBridgeなど)の場合は、Lambda関数の送信先(失敗時)でSNSに送信して、メールやSlackに送る。
同期(APIなど)の場合は、CloudWatch Logsのサブスクリプションフィルターで通知用のLambdaに送信して、そこからSNSに送信して、メールやSlackに送る。
また、通知用のLambdaも非同期なので、非同期のパターンと同様にして、この関数自体の例外エラーも通知するのがベストです。

構成検討

パターンはこちらを参考に、非同期以外にも同期もあるので、ケース毎に検討してみました。
Lambda でバッチ処理を構築する際のエラー通知パターン 5選 – サーバーワークスエンジニアブログ

今まで作成したものを分類

非同期(S3トリガーやEventBridgeなど)

・createMediaConvertJob ← S3
・receiveMediaConvertJobState ← EventBridge (CloudWatch Events)

同期(APIなど)

・mediaConvertInputFileToJobIdApi ← API GatewayまたはApplication Load Balancer
・mediaConvertJobIdToStatusApi ← API GatewayまたはApplication Load Balancer
・mediaConvertInputFileToJobIdEdgeApi ← CloudFront(Lambda@Edge)
・mediaConvertJobIdToStatusEdgeApi ← CloudFront(Lambda@Edge)

パターン1. 直接Publish

Lambda関数で、例外をキャッチして、コードでSNSに送る方法。
エラー通知の為のSNSに繋がらなかったら通知できないので却下。

パターン2. DLQ(デットレーターキュー)

Lambda関数

デットレーターキューが動くのは非同期(S3トリガーやEventBridgeなど)のみなので、
同期(APIなど)では設定しても送信されない。テストも同期。

Lambda@Edgeでは設定されていると、関数の関連付けでエラーになります。
> The function cannot have a Dead Letter Queue configuration. Function: arn:aws:lambda:us-east-1:xxxxxxxxxxxx:function:mediaConvertJobIdToStatusEdgeApi:3

また、SNSからメッセージ整形でLambda使うと、このLambdaのエラー通知には使えない。
(ループになる)

メッセージ整形せずに直接SNSに送る事もできますが、Lambda関数名が表示されず、
スタックトレースが表示される訳でもなく、呼び出し元の情報なので、
何が、どこでエラーになったかの切り分けが難しい。



パターン3. 送信先(失敗時)

Lambda関数

こちらも非同期(S3トリガーやEventBridgeなど)のみですが、
Lambda関数名(リージョンも)やスタックトレースも(呼び出し元の情報も)表示されるのでOK!

パターン2のDLQもですが、再施行(リトライ)回数を超えた後に通知されます。
JSONなので見難いですが、わざわざLambdaで整形するのも障害ポイント増えるだけなので、
やらない方が良さそう。来たら対応して来ないようにするのが目的なので。



パターン4. メトリクスフィルター

Lambdaのログに特定の文字が含まれる数をCloudWatch Alarmで監視してSNSに通知する方法。
期間(デフォルト5分)のエラー数を通知するので、
即時性もないし、スタックトレースが表示される訳でもない。

CloudWatch ロググループ



また、アラームは期間内にエラーなしの場合「データ不足」、エラーありの場合「アラーム状態」となるだけなので、アラームが無くなっても復旧した訳ではない。
(設定の「欠落データの処理」を変更した場合は「OK」にしたり「アラーム状態」を維持できる)

通知だけの為に、アラームにコストかけるのはもったいないと思う。1個のアラーム辺り$0.10/月
料金 – Amazon CloudWatch | AWS

追加. サブスクリプションフィルター+送信先(正常)

サブスクリプションフィルターもメトリクスフィルターと同様のフィルタリングができる。
違いは送信先で、LambdaやKinesisなどに送れる。(メトリクスフィルターはAlarmのみ)

CloudWatch ロググループ

通知用のLambda関数(notifyErrorLog)

import base64
import gzip
import json

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

    messages = []
    data = json.loads(gzip.decompress(base64.b64decode(event['awslogs']['data'])))
    for log_event in data['logEvents']:
        messages.append(log_event['message'].replace('\u00a0', ' ').split('\n'))

    body = { 'owner': data['owner'], 'logGroup': data['logGroup'], 'logStream': data['logStream'], 'message': messages }
    print(body)
    return body

パターン3の送信先(失敗時)のメール内容とある程度揃える為、加工しています。

returnが送信先(正常)に送られます。
失敗時も設定。パターン3の送信先(失敗時)と同じ動きになります。

必要な情報もあり、一見良さそうなのですが、フィルター(“[ERROR] “)が正規表現に対応していない為、通知したくないものも通知されてしまいます。
例えば、パラメータにフィルターと同じ値が設定され、ログに出力している場合とか。
> {‘input_file’: ‘Big Buck Bunny.mp4’}
> {‘input_file’: ‘[ERROR] xxx’}
> [ERROR] NameError: name ‘xxx’ is not defined”, “Traceback (most recent call last):



パターン5. サブスクリプションフィルター

追加のサブスクリプションフィルター+送信先(正常)で記載したものと似た構成で、違いは送信先(正常)を設定していないのと、Lambda関数のコードの違い。
あと、環境変数で作成したSNSトピックのARNを設定しています。

SNS_TOPIC_ARN: arn:aws:sns:ap-northeast-1:【アカウントID】:LambdaError
import boto3
import os
import base64
import gzip
import json

sns = boto3.client('sns')
SNS_TOPIC_ARN = os.environ['SNS_TOPIC_ARN']
SNS_SUBJECT_PREFIX = '[WARNING]notifyErrorLog report for '
PREFIX_FILTER = '[ERROR] '
AWS_REGION = os.getenv("AWS_REGION")

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

    messages = []
    data = json.loads(gzip.decompress(base64.b64decode(event['awslogs']['data'])))
    for log_event in data['logEvents']:
        if log_event['message'].startswith(PREFIX_FILTER):
            messages.append(log_event['message'].replace('\u00a0', ' ').split('\n'))
    if messages == []:
        return

    log_group = data['logGroup']
    body = { 'owner': data['owner'], 'region': AWS_REGION, 'logGroup': log_group, 'logStream': data['logStream'], 'message': messages }
    print(body)

    print(sns.publish(
        TopicArn = SNS_TOPIC_ARN,
        Subject = SNS_SUBJECT_PREFIX + log_group,
        Message = json.dumps(body)
    ))

リージョンも追加しました。

スタックトレースやログの場所も表示できるのでOK!同期(APIなど)に適している。
非同期(S3トリガーやEventBridgeなど)だと再施行(リトライ)毎に通知されてしまう。



まとめ

非同期(S3トリガーやEventBridgeなど): パターン3. 送信先(失敗時)
同期(APIなど): パターン5. サブスクリプションフィルター
Lambdaの例外エラーをメールとSlackに通知する

備えあれば憂いなし。ちゃんと設定されてて通知が来ないのと、そもそも設定がなくて通知が来ないのでは安心感が違いますね。
導入時にはちゃんとテストするものの、改修を重ねて行くと動かなくなる事も良くあるので、定期的にテストしたり、AWS Configとかで変更を監視したりもしたいですね。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です