CloudWatch LogsのログアラートをEventBridge API Destination経由でSlack通知する

CloudWatch Logsのログイベントを直接サブスクリプションで処理するのではなく、Metric Filter→CloudWatch Alarm→EventBridge API Destination →Slack という流れで通知を実現する方法を紹介します。

ねらい

CloudWatch Logsのログイベントでアラート通知するようなケースでは、Subscription Filterを使用してLambdaを経由する方法がよくある方法だと思います。

しかし、以下のようなニーズがある場合はLambdaを経由するSubscription Filterの仕組みが採用しづらいかもしれません。

  • ノーコードで仕組みを組み立てたい
  • ランタイム更新やライブラリ脆弱性などの保守対応を減らしたい

こうした要件がある場合に、この仕組みはシンプルにログアラート通知を実現する選択肢となり得ます。

全体構成

本記事では以下の構成をterraformで実装してみます。

Terraform例

作成したterraform構成は次のとおりです。

.
├── CloudWatch.tf
├── EventBridge.tf
├── Lambda_app.tf
├── provider.tf
├── src
│   └── index.py
└── variables.tf

Lambda_app.tf

### Lambda
module "lambda_function" {
source  = "terraform-aws-modules/lambda/aws"
version = "8.1.0"

function_name = "lambda_app"
handler       = "index.lambda_handler"
runtime       = "python3.13"

source_path = "./src"

publish = true
}

公式のterraformモジュールを使用してLambda関数を作成します。

Lambdaのソースコードは次のとおりです。

  • ./src/index.py
def lambda_handler(event, context):
  print("TEST_ERROR: simulated failure")

テスト用にログを吐くだけのLambdaなので、極めてシンプルなコードを作成しています。

CloudWatch.tf

### CloudWatch
module "log_metric_filter" {
  source  = "terraform-aws-modules/cloudwatch/aws//modules/log-metric-filter"
  version = "5.7.1"

  log_group_name = module.lambda_function.lambda_cloudwatch_log_group_name

  name    = "error-metric"
  pattern = "TEST_ERROR"

  metric_transformation_namespace = var.metrics_namespace
  metric_transformation_name      = var.metrics_name
}

module "metric_alarm" {
  source  = "terraform-aws-modules/cloudwatch/aws//modules/metric-alarm"
  version = "5.7.1"

  alarm_name          = "lambda-app-logs-errors"
  comparison_operator = "GreaterThanOrEqualToThreshold"
  evaluation_periods  = 1
  threshold           = 1
  period              = 60

  namespace          = var.metrics_namespace
  metric_name        = var.metrics_name
  statistic          = "Sum"
  treat_missing_data = "notBreaching"
}

公式のterraformモジュールを使用して、CloudWatch関連リソースを作成しています。
アプリケーションが吐き出したログの中に”TEST_ERROR”という文字列が含まれていた場合、CloudWatchメトリクスに変換し、メトリクスがカウントアップされたらCloudWatch アラームを発報する仕組みを構築します。

そのために必要なリソースは、以下の3つです。

  • CloudWatch Logsロググループ
  • メトリクスフィルター
  • CloudWatch Alarm

これらのうち、ロググループはLambdaを作成した時に同時に作成されています。

上述のコードでは作成されたロググループに対して、メトリクスフィルターを作成しています。

patternの値によってメトリクスの生成対象とするログの文字列を指定します。この文字列に一致するログが出力された時、メトリクスがカウントアップされます。

また、メトリクスの名前空間(metric_transformation_namespace)と、メトリクス名(metric_transformation_name)を指定することができます。

これらの値はCloudWatch Alarmのパラメータと合わせる必要があります。

CloudWatch Alarmは、TEST_ERRORに合致するログが一度でも出現したらアラートを発報する設定にしていますので、thresholdを1、comparison_operatorにGreaterThanOrEqualToThresholdを指定します。
これらのパラメータは適切なものを設定するようにしてください。

また、メトリクスフィルターで生成されるメトリクスは、該当の文字列が出現しない場合はメトリクス値無し(Missing)になるため、treat_missing_dataはnotBreachingとする(値無しは正常とみなす)のをおすすめします。

EventBridge.tf

### EventBridge
module "eventbridge_api_destination" {
  source  = "terraform-aws-modules/eventbridge/aws"
  version = "4.1.0"

  create_bus                    = false
  create_connections            = true
  create_api_destinations       = true
  attach_api_destination_policy = true

  connections = {
    slack = {
      authorization_type = "API_KEY"
      auth_parameters = {
        api_key = {
          key   = "Authorization"
          value = "Bearer ${var.slack_info.api_token}"
        }

      }
    }
  }

  api_destinations = {
    slack = {
      invocation_endpoint = "https://slack.com/api/chat.postMessage"
      http_method         = "POST"
    }
  }

}

module "eventbridge_rule" {
  source  = "terraform-aws-modules/eventbridge/aws"
  version = "4.1.0"

  create_bus     = false
  create_rules   = true
  create_targets = true
  create_role    = false

  rules = {
    slack = {
      event_pattern = jsonencode({
        source      = ["aws.cloudwatch"]
        detail-type = ["CloudWatch Alarm State Change"]
        detail = {
          state = {
            value = ["ALARM"]
          }
        }
        resources = ["${module.metric_alarm.cloudwatch_metric_alarm_arn}"]
      })
    }
  }

  targets = {
    slack = [{
      name            = "slack"
      arn             = module.eventbridge_api_destination.eventbridge_api_destination_arns["slack"]
      attach_role_arn = module.eventbridge_api_destination.eventbridge_role_arn
      input_transformer = {
        input_paths = {
          AlarmName   = "$.detail.alarmName",
          MetricName  = "$.detail.configuration.metrics[0].metricStat.metric.name",
          Namespace   = "$.detail.configuration.metrics[0].metricStat.metric.namespace",
          Region      = "$.region",
          StateReason = "$.detail.state.reason",
          StateValue  = "$.detail.state.value"
        }
      input_template = <<EOF
      {
        "channel": "${var.slack_info.channel_id}",
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": ":rotating_light: *<https://<Region>.console.aws.amazon.com/cloudwatch/home?region=<Region>#alarmsV2:alarm/<AlarmName>|CloudWatch Alarm: <AlarmName>>*"
            }
          },
          {
            "type": "section",
            "fields": [
              { "type": "mrkdwn", "text": "*State*\n<StateValue>" },
              { "type": "mrkdwn", "text": "*Namespace*\n<Namespace>" },
              { "type": "mrkdwn", "text": "*Metric*\n<MetricName>" }
            ]
          },
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "*Reason*\n<StateReason>"
            }
          }
        ]
      }
      EOF
      }

    }]
  }

}

こちらも公式のterraformモジュールを使用して作成しています。
EventBridge系のリソースはIAMロールなどのリソースも付随的に作成する必要があります。公式モジュールを使用すればIAMロールなどの関連するリソースも漏れなく作成してくれるのでおすすめです。

以下の3つのEventBridgeリソースを上記のコードで作成しています。

  • EventBridge API destinations
  • EventBridge Connections
  • EventBridge Rule

eventbridge_api_destinationのモジュールでは、EventBridge API destinationsとEventBridge Connectionsのリソースを作成します。

API destinationsとは、呼び出すエンドポイントURLとHTTPメソッドを指定することで、外部のSaaSと連携を可能にするEventBridgeリソースです。

Connectionsは送信先APIに対する認証情報を安全に管理するためのリソースです。API destinationsと関連づけることで使用します。
認証情報などの値はterraformの変数機能などを活用し、ハードコードしないように注意してください。

今回はslackに通知を送信することから、指定したslackのチャンネルにメッセージを投稿するためのエンドポイント”https://slack.com/api/chat.postMessage”をAPI destinationsとして作成します。このAPIを実行するためには作成したslackアプリケーションにchat:writeのスコープを追加する必要があるので注意が必要です。

そして、Connectionsには、slackのBot User OAuth Tokenを登録しています。アプリケーションがslackのチャンネルにメッセージを投稿するための認証情報のようなものです。

eventbridge_ruleのモジュールでは、EventBridge Ruleのリソースを作成します。

event_patternに一致するイベントが発生したときに、slack通知を行うための条件をevent_patternに定義します。

そして、ターゲットにeventbridge_api_destinationで作成したAPI destinationsを指定することでslack通知を行います。

event_patternは「CloudWatch AlarmがALERT状態に遷移した時」としています。

targetにはAPI destinationを指定しており、input transformerというパラメータを指定します。

このパラメータでは、EventBridgeが受け取ったアラーム状態遷移イベントから必要な値を抜き出し、slackにポストするメッセージボディを作成するために使用します。
具体的には、input_templateに記述しているJSONがSlackのメッセージとして解釈されます。slackのBlock Kit Builderを使うと簡単にJSONが作成できるので活用すると良いと思います。

variables.tf

variable "aws_region" {
type        = string
description = "AWS region"
default     = "ap-northeast-1"
}

variable "metrics_namespace" {
type        = string
description = "CloudWatch Metrics Namespace"
default     = "lambda_app"
}

variable "metrics_name" {
type        = string
description = "CloudWatch Metrics Name"
default     = "ErrorCount"
}

variable "slack_info" {
type = object({
  channel_id = string
  api_token  = string
})
sensitive = true
}

このサンプルでは、複数回使用されるmetrics_namespaceやmetrics_name、そして連携先slackの情報を変数化しています。

実運用の際は適切に変数を定義してください。

注意点として、slackの認証情報についてはsensitiveのパラメータをtrueとし、terraformコマンドを実行した時に不用意に表示させないようにすることをおすすめします。

動作確認

terraform applyでリソースを作成して、実際にslackに通知がくることを確認します。

作成したLambdaの実行テストを行えば、エラーを想定したログがロググループに対して出力されます。

テストが実行されると、「TEST_ERROR: simulated failure」の文字列がログに出力されます。

これはEventBridgeのイベントパターンに合致するため、1,2分後くらいにslackに通知のメッセージが送信されるはずです。

通知の内容は該当のCloudWatchアラームへのリンクやメトリクスの情報、アラート状態になった理由などを表示するようにしています。

メッセージ内容は、EventBridge Ruleの作成時に指定したinput transformerの設定で自由にカスタマイズすることが可能です。

アラームの画面も確認してみると、ALERT状態になっていることが確認できます。

EventBridge Ruleのページをみると、Matched Event、Invocationsのメトリクスが上がっています。
EventBridgeが実行されたことも確認できました。

コメント

タイトルとURLをコピーしました