MNTSQ Techブログ

リーガルテック・カンパニー「MNTSQ(モンテスキュー)」のTechブログです。

DynamoDBへの書き込みをLambdaを使わずにSlack通知したかった

MNTSQ Tech Blog TOP > 記事一覧 > DynamoDBへの書き込みをLambdaを使わずにSlack通知したかった

......のですが、かなり苦戦しました。この記事に辿り着いた人はすでにハマっている、もしくはこれからハマる運命(さだめ)にある人も多いと思うので、そのような人の助けになればと思い、記事にして残しておきます。

結論からお伝えすると、Lambdaを使わずに通知を行うことは可能ですが、設定は少し複雑かなという印象でした。 しかし、一度設定出来てしまえば、同じようなことをしたい時の実装コストをグッと抑えられる、とても良い仕組みだと思います。

構成について

Lambdaではなく、AWS Chatbotを使用してSlackへの通知を行う構成です。

この構成のメリット
  • Lambdaのランタイムやコードの管理から解放される
  • SNS -> Chatbot -> Slack」,「EventBridgeRule -> SNS」 の部分の汎用性が高く、使いまわせる
この構成のデメリット
  • メッセージのカスタマイズ性に少しだけ欠ける
  • 初めて設定する際はハマりどころが多い

AWS Chatbotの認証を行う

まずは、AWS ChatbotからSlackワークスペースに対しての認証を行います。 あらかじめSlackワークスペースにChatbotのアプリをインストールしておく必要があります。 mntsq.slack.com

ワークスペースにアプリをインストールしたら、Chatbotが通知を行いたいチャンネルにアプリを招待します。チャンネル詳細の「インテグレーション」タブから、「AWS Chatbot」のアプリを招待します。

次に、AWSコンソールからChatbotのページへ行き、Slackクライアントの設定を行います。

ブラウザでSlackログインしていた場合、Slackの認証ページにリダイレクトされるので、「承認」します。

その後「Slack チャネルを設定」という編集画面に遷移することがありますが、今回はterraformでデプロイを行うので、この画面は閉じてしまって大丈夫です。

terraformでデプロイしてみる

今回はこのような仕組みを作るものとします。

  • ファイルアップロード時にウイルススキャンを行い、感染ファイルが見つかった場合、DynamoDBのInfectedScanResultsというテーブルに書き込みを行う
  • InfectedScanResultsに書き込みがあった場合、その内容をSlackのエラーチャンネルに通知する

SNS -> Chatbot -> Slackの部分

まずは通知の起点となるSNSトピックとChatbotのチャンネル設定を作成します。

SNS

# 後段のChatBotからSlackへの通知を行うためのSNSトピック
resource "aws_sns_topic" "slack_notify_error" {
  name = "slack-notify-error"
}

# SNSにCloudWatchからのPublishを許可するポリシー
data "aws_iam_policy_document" "allow_cloudwatch_to_publish_sns" {
  statement {
    actions = [
      "sns:Publish",
    ]
    effect = "Allow"
    resources = [
      aws_sns_topic.slack_notify_error.arn,
    ]
    principals {
      type        = "Service"
      identifiers = [
        "cloudwatch.amazonaws.com",
      ]
    }
  }
}

resource "aws_sns_topic_policy" "allow_cloudwatch_to_publish_sns" {
  arn    = aws_sns_topic.slack_notify_error.arn
  policy = data.aws_iam_policy_document.allow_cloudwatch_to_publish_sns.json
}

Chatbot

# chatbot用のIAMロール
data "aws_iam_policy_document" "chatbot_assume_policy" {
  statement {
    effect = "Allow"
    principals {
      type        = "Service"
      identifiers = ["chatbot.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

data "aws_iam_policy_document" "chatbot_slack_notify" {
  statement {
    effect = "Allow"
    actions = [
      "sns:Subscribe",
      "sns:ListSubscriptionsByTopic",
      "sns:GetTopicAttributes",
      "sns:Publish"
    ]
    resources = [
      aws_sns_topic.slack_notify_error.arn
    ]
  }
}

resource "aws_iam_role" "chatbot_slack_notify_error" {
  name               = "chatbot-slack-notify-error-role"
  assume_role_policy = data.aws_iam_policy_document.chatbot_assume_policy.json
}

resource "aws_iam_policy" "chatbot_slack_notify_error" {
  name        = "chatbot-slack-notify-error-policy"
  description = "IAM policy for Chatbot to notify error to Slack"
  policy      = data.aws_iam_policy_document.chatbot_slack_notify.json
}

# Chatbot  チャンネル設定
resource "aws_chatbot_slack_channel_configuration" "slack_notify_error" {
  configuration_name = "slack-notify-error"
  slack_channel_id   = "YOUR_CHANNEL_ID"
  slack_team_id      = "YOUR_WORKSPACE_ID"

  logging_level = "INFO"

  sns_topic_arns = [
    aws_sns_topic.slack_notify_error.arn
  ]

  iam_role_arn = aws_iam_role.chatbot_slack_notify_error.arn
}

terraform applyを実行したら、AWSコンソールからリソースが作成されたことを確認してください。 Chatbotのコンソールから「テストメッセージの送信」を行い、Slackチャンネルに通知が届けばOKです。

※弊社開発環境のものなので、微妙にリソース名が異なります

なお、2024/12月現在、Chatbotのロググループは強制的にバージニア北部に作成されるので、東京リージョンを彷徨わないようにご注意下さい。

DynamoDB Stream -> EventBridgePipes -> EventBridgeCustomBusの部分

DynamoDBに書き込みがあった時、それをCustomBusにEventとして送信するEventBridgePipesの設定を行います。

DynamoDB

## InfectedScanResultsテーブル
resource "aws_dynamodb_table" "infected_scan_results" {
  name         = "InfectedScanResults"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "ObjectPath"
  attribute {
    name = "ObjectPath"
    type = "S"
  }

  # EventBridge -> SNS -> Slack通知を行うためのストリーム
  stream_enabled   = true
  stream_view_type = "NEW_IMAGE"
}

EventBridge

# カスタムのイベントを記録するためのバス
resource "aws_cloudwatch_event_bus" "notification" {
  name = "notification"
}

# PipeのためのIAMロール
data "aws_iam_policy_document" "eventbridge_pipe_assume_role_policy" {
  statement {
    actions = ["sts:AssumeRole"]
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["pipes.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "dynamodb_pipe_role" {
  name               = "dynamodb-pipe-role"
  assume_role_policy = data.aws_iam_policy_document.eventbridge_pipe_assume_role_policy.json
}

data "aws_iam_policy_document" "dynamodb_pipe_policy" {
  statement {
    actions = [
      "dynamodb:DescribeStream",
      "dynamodb:GetRecords",
      "dynamodb:GetShardIterator",
      "dynamodb:ListStreams",
    ]
    effect    = "Allow"
    resources = [aws_dynamodb_table.infected_scan_results.stream_arn]
  }

  statement {
    actions = [
      "events:PutEvents"
    ]
    effect    = "Allow"
    resources = ["*"]
  }

  statement {
    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents"
    ]
    effect    = "Allow"
    resources = ["*"]
  }
}

resource "aws_iam_role_policy" "dynamodb_pipe_policy" {
  name   = "dynamodb-pipe-policy"
  role   = aws_iam_role.dynamodb_pipe_role.name
  policy = data.aws_iam_policy_document.dynamodb_pipe_policy.json
}

# ロググループ
resource "aws_cloudwatch_log_group" "dynamodb_infected_scan_results_write" {
  name   = "/aws/vendedlogs/pipes/dynamodb-infected-scan-results-write"
}

# InfectedScanResultsのストリームをEventBridgeに通知するPipe
resource "aws_pipes_pipe" "dynamodb_infected_scan_results_write" {
  name     = "dynamodb-infected-scan-results-write"
  role_arn = aws_iam_role.dynamodb_pipe_role.arn
  source   = aws_dynamodb_table.infected_scan_results.stream_arn
  target   = aws_cloudwatch_event_bus.notification.arn

  log_configuration {
    include_execution_data = ["ALL"]
    level                  = "INFO"
    cloudwatch_logs_log_destination {
      log_group_arn = aws_cloudwatch_log_group.dynamodb_infected_scan_results_write.arn
    }
  }

  source_parameters {
    dynamodb_stream_parameters {
      batch_size        = 1
      starting_position = "LATEST"
    }
  }

  target_parameters {
    eventbridge_event_bus_parameters {
      detail_type = "InfectedScanResultsWrite"
      source      = "custom.dynamodb.infected-scan-results"
    }
  }
}

terraform applyを実行したら、DynamoDBにレコードを追加してみてください。ロググループ/aws/vendedlogs/pipes/dynamodb-infected-scan-results-writedynamodb-infected-scan-results-writeというストリームが作成され、いくつかログが出ているはずです。

# 最後のログがこんな感じだったらOK
{
    "resourceArn": "arn:aws:pipes:ap-northeast-1:******:pipe/dynamodb-infected-scan-results-write",
    "timestamp": 1734671733500,
    "executionId": "8e3e7d3c-0c1e-4b47-a6b7-******",
    "messageType": "ExecutionSucceeded",
    "logLevel": "INFO"
}

ちなみにこのイベントはCroudTrailなどには記録されません。(最初はCroudTrailに記録されるものだと勘違いして時間を溶かしました)EventBridgeのコンソールからここら辺確認できるようになるとありがたいなぁ......と、しみじみ思います。

EventBridgeRule -> SNS の部分

最後に、CustomBusにイベントが送信された時に、それを拾ってSNSにパブリッシュするRuleを作成します。Chatbotが受け取ることができるjsonの形式は決まっているので、EventBridgeの入力トランスフォーマを使用し、良い感じに整形します。

EventBridgeRule

# busのイベントをSNSに通知するためのルール
resource "aws_cloudwatch_event_rule" "dynamodb_infected_scan_results_write" {
  name        = "dynamodb-infected-scan-results-write"
  description = "Send DynamoDB InfectedScanResults write events to EventBridge"

  event_bus_name = aws_cloudwatch_event_bus.notification.name

  event_pattern = jsonencode({
    source      = ["InfectedScanResultsPuts"]
    detail-type = ["custom.dynamodb.infected-scan-results"]
  })
}

resource "aws_cloudwatch_event_target" "dynamodb_infected_scan_results_write_target" {
  rule           = aws_cloudwatch_event_rule.dynamodb_infected_scan_results_write.name
  arn            = aws_sns_topic.slack_notify_error.arn
  event_bus_name = aws_cloudwatch_event_bus.notification.name
  input_transformer {
    input_paths = {
      "ObjectPath" : "$.detail.dynamodb.NewImage.ObjectPath.S",
      "ScannedAt" : "$.detail.dynamodb.NewImage.ScannedAt.S",
      "Message" : "$.detail.dynamodb.NewImage.Message.S"
    }
    # jsonencodeを使用すると<, >などが文字コードに変換されてしまうのでTEMPLATEを使用する
    input_template = <<TEMPLATE
{
  "version": "1.0", 
  "source": "custom",   
  "content": {
      "textType": "client-markdown", 
      "title": "⚠️ウイルス感染ファイルが検出されました⚠️",  
      "description": "<!subteam^YOUR_TEAM_ID>\n<ObjectPath>\n<ScannedAt>\n<Message>"
  }
}
TEMPLATE
  }
}

<!subteam^YOUR_TEAM_ID>の部分はメンションしたいSlackのTeamIDです。TeamIDの確認の仕方はここでは割愛します。Chatbotが解釈できるjsonの形式は、こちらのドキュメントを参照しました。

これで全リソースの定義を作成できました。 terraform applyを実行して、もう一度DynamoDBにレコードを追加し、Slackに通知が飛んでくることを確認しましょう。

無事通知されました!

おわりに

今回はLambdaを使用せず、DynamoDBへの書き込みをSlackに通知する方法について紹介させていただきました。EventBridge + SNS + Chatbotの構成は設定も簡単で再利用性が高く、監視モニタリングの整備をする際にはとても便利な仕組みですね。

本当はSlackのメッセージにカラーバーをつけたりとカスタマイズしたかったのですが、AWSサポートに問い合わせたところ、2024/12現在ではそこまで細かい設定はできないようです。ただしEventBridgeRuleのターゲットにはHTTPエンドポイントも指定できるようなので、もっとこだわりたい人はこちらの方法を使ってみるのも良いかもしれません。

dev.classmethod.jp

以上、何かの助けになれば幸いです!

MNTSQ株式会社 SRE 西室