Lambdaの実行エラーをSlackに通知するCDKを作ってみた

この記事では、AWS Lambdaで発生したエラーをSlackに通知する仕組みを、AWS CDKで構築する方法を紹介します。Lambdaなどのアプリケーションコードを使わずに通知する仕組みを作ることで、エラー通知システムのデバッグをせずに済む方法を模索しました。

広告ここから
広告ここまで

目次

    この記事では、AWS Lambdaで発生したエラーをSlackに通知する仕組みを、AWS CDKで構築する方法を紹介します。Lambdaなどのアプリケーションコードを使わずに通知する仕組みを作ることで、エラー通知システムのデバッグをせずに済む方法を模索しました。

    欲しかったもの

    EventBridgeを使ってスケジュールバッチを作ろうとしています。その中でバッチが失敗したことを確実に検知する仕組みを作りたいと思いました。API系であれば、エラーレスポンスを返す際にSNSやエラートラッキングツールへデータを送信できますし、CloudWatchでHTTPレスポンスステータスコードを見ることができます。ただ、バッチ処理、それもライブラリの兼ね合いで慣れない言語であるPythonで実装されたもののエラー通知を確実に受け取りたいと考えたため、次の点を重視した通知機構を作る事にしました。

    1. Lambdaの外側で通知を出す
    2. 最低限「何かが起きたことがわかる」ものであればよいとする。
      調査などはCloudWatch Logsを見れば良いと割り切る
    3. 実行頻度の多くない関数なので、1回でもエラーが出たら即通知を飛ばす
    4. なるべく保守をしたくないので、IaC以外の用途でコードは書きたくない

    今回実装したもの

    いろいろと手法を検討した結果、次のフローを採用しました

    • LambdaのログをJSONで出力
    • Logsのメトリクスフィルターでエラーを検知
    • メトリクスフィルターを使ってCloudWatchアラームをセット
    • SNS -> ChatbotでSlack通知

    CloudWatchのアラームで通知を出す仕組みのため、アラーム中に追加のエラーが発生した場合の検知などは難しいと思われます。ですが1日1・2回程度の実行頻度である処理で、再実行は実装者本人が手動で行う想定とすれば充分であると判断しました。

    CDK Constructとして実装してみた

    今回の通知機構は他のバッチ処理でも使う可能性が高いので、Construct化しています。SNS -> Chatbot部分については、すでに稼働中のものがあるため、SNSに通知するまでの部分だけを定義しました。

    import * as cdk from 'aws-cdk-lib';
    import { Construct } from 'constructs';
    import * as lambda from 'aws-cdk-lib/aws-lambda';
    import * as sns from 'aws-cdk-lib/aws-sns';
    import * as logs from 'aws-cdk-lib/aws-logs';
    import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
    import * as cw_actions from 'aws-cdk-lib/aws-cloudwatch-actions';
    
    interface LambdaMonitoringProps {
      // 外部で定義されたLambda関数
      function: lambda.IFunction;
      
      // モニタリングの設定
      notificationTopicArns: string[];
      metricNamespace: string;
      alarmDescription?: string;
      errorThreshold?: number;
      evaluationPeriods?: number;
    }
    
    export class LambdaMonitoring extends Construct {
      public readonly logGroup: logs.LogGroup;
      public readonly errorAlarm: cloudwatch.Alarm;
    
      constructor(scope: Construct, id: string, props: LambdaMonitoringProps) {
        super(scope, id);
    
        // CloudWatch Logsグループの作成
        this.logGroup = new logs.LogGroup(this, 'LogGroup', {
          logGroupName: `/aws/lambda/${props.function.functionName}`,
          retention: logs.RetentionDays.TWO_WEEKS,
        });
    
        // エラーのメトリクスフィルターの作成
        const errorFilter = new logs.MetricFilter(this, 'ErrorMetricFilter', {
          logGroup: this.logGroup,
          filterPattern: logs.FilterPattern.stringValue('$.log_level', '=', 'ERROR'),
          metricNamespace: props.metricNamespace,
          metricName: `${props.function.functionName}ErrorCount`,
          defaultValue: 0,
          metricValue: '1',
          unit: cloudwatch.Unit.COUNT,
        });
    
        // CloudWatchアラームの作成
        this.errorAlarm = new cloudwatch.Alarm(this, 'ErrorAlarm', {
          metric: errorFilter.metric({
            statistic: 'sum',
            period: cdk.Duration.minutes(5),
          }),
          threshold: props.errorThreshold ?? 0,
          evaluationPeriods: props.evaluationPeriods ?? 1,
          alarmDescription: props.alarmDescription || `Alarm when an error occurs in ${props.function.functionName}`,
        });
        props.notificationTopicArns.forEach((notificationTopicArn, i) => {
            // 既存のSNSトピックの参照
            const notificationTopic = sns.Topic.fromTopicArn(
              this,
              'NotificationTopic' + i,
              notificationTopicArn
            );
        
            // アラームにSNSアクションを追加
            this.errorAlarm.addAlarmAction(new cw_actions.SnsAction(notificationTopic));
        })
      }
    }

    SNSの通知先を複数指定できるようにするため、ARNを配列で渡してループにしています。もしかするとSNS Topicそのものを引数にする方が良いかもしれません。そうすればSNSも新しく作成するケースでもいちいちARNを取得してConstructに渡してまたfromTopicArnして・・・と二度手間のような処理を通す必要がなくなります。

    constructの使い方

    作成したConstructは、LambdaのCDKリソースと併せて使います。SNSのARNもしくはインスタンスを配列で渡せるようにして、そのうちnpmに公開したりするかもしれません。

        const weatherForecastLambda = new lambda.Function(this, 'EorzeaWeatherForecast', {
          runtime: lambda.Runtime.PYTHON_3_12,
          handler: 'lambda_forecast.handler',
          code: lambda.Code.fromAsset('lambda'),
          timeout: cdk.Duration.seconds(10),
          loggingFormat: lambda.LoggingFormat.JSON,
        })
        new LambdaMonitoring(this, 'EorzeaWeatherForecastMonitoring', {
          function: weatherForecastLambda,
          notificationTopicArns: [props.SLACK_NOTIFICATION_SNS_ARN, props.EMAIL_NOTIFICATION_SNS_ARN],
          metricNamespace: 'EorzeaWeatherForecas',
        })

    今の所目的自体は達成できているので、しばらく様子見する予定です。他のプロジェクトでも使おうとなれば、コードの共有手間を省くことも兼ねてnpmへ公開しようと思います。

    余談: CloudWatch AlarmをAWS CLIでテストする

    このConstructのテストをするために調べて知ったのですが、アラーム状態を手動で変更できるみたいです。—state-valueOKにするとアラーム状態から戻ります。

    aws cloudwatch set-alarm-state \
        --alarm-name <CWのアラーム名>  \
        --state-value OK \
        --state-reason "Testing alarm after policy update" \
        --profile <プロファイル名> \
        --region ap-northeast-1

    その後ALARMをセットすると、再びアラーム状態にできます。

    aws cloudwatch set-alarm-state \
        --alarm-name <CWのアラーム名>  \
        --state-value ALARM \
        --state-reason "Testing alarm after policy update" \
        --profile <プロファイル名> \
        --region ap-northeast-1

    上手くいけば、AWS Chatbot経由でSlackに通知が飛んでくるはずです。

    次のステップ

    まだLambdaをスケジュール実行していないので、その辺りの準備が整ったら評価期間にようやく入ります。うまく使えそうかつコストが嵩まない様子ならば、バッチ処理系はこの仕組みベースにしようかなと思います。

    理想的には、Bedrockにエラーログを渡して分析させるようなこともしたいなぁとは思いますが、それはまた別の話。

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark