......のですが、かなり苦戦しました。この記事に辿り着いた人はすでにハマっている、もしくはこれからハマる運命(さだめ)にある人も多いと思うので、そのような人の助けになればと思い、記事にして残しておきます。
結論からお伝えすると、Lambdaを使わずに通知を行うことは可能ですが、設定は少し複雑かなという印象でした。 しかし、一度設定出来てしまえば、同じようなことをしたい時の実装コストをグッと抑えられる、とても良い仕組みだと思います。
構成について
Lambdaではなく、AWS Chatbotを使用してSlackへの通知を行う構成です。
この構成のメリット
この構成のデメリット
- メッセージのカスタマイズ性に少しだけ欠ける
- 初めて設定する際はハマりどころが多い
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のチャンネル設定を作成します。
# 後段の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-write
にdynamodb-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エンドポイントも指定できるようなので、もっとこだわりたい人はこちらの方法を使ってみるのも良いかもしれません。
以上、何かの助けになれば幸いです!
MNTSQ株式会社 SRE 西室