はじめに
CloudWatchメトリクスに対しAlarmを仕掛け、SNS、AWS Chatbotを介してSlackへ、メトリクスの状況変化の発報を行うという構成は、皆さんお使いかと思います。
ただ、Alarmの特性上、ディメンションを指定したメトリクスを対象にすることができないため、この形での発報だけでは、例えばどのEC2インスタンスのCPU利用率が閾値を超えたか、といった、ディメンションのどの対象が閾値を超えたか、をSlack上から知ることはできない、ということになります。
そこで、このディメンションの情報のSlackへの連携を、保守コストの高いアプリケーションロジックの実装(Lambdaなど)なしに実現する、をゴールとし、本記事をお届けします。
構成
- EventBridgeルールで、対象のCloudWatch Alarmに対し、CloudWatch Alarm State Changeで、
ALARM
状態への変更時にStep Functionsステートマシンを呼び出す - Step Functionsステートマシンで、
GetMetricData
タスクで目的のメトリクス値を取得し、Call HTTPS APIs
タスクでSlackのIncoming Webhookへ送信
ということをやっています。
EventBridge
EventBridgeでは、イベントパターンとターゲットを指定していきます。イベントバスは、こだわりが無ければデフォルトイベントバスでよいかと思います。
イベントパターンは、今回は以下のようにいたしました。
source
、detail-type
それぞれ対象のAWSサービス、イベントタイプを指定する固定値で、detailにユーザー側で指定する値を含めます。
筆者の例では、特定の複数の強権限ロールのいずれかへのAssumeRoleがあった場合にALARM
状態になるCloudWatch AlarmDetect-AssumeRole-To-PowerRole
について、ALARM
状態に変化した場合のみ、をトリガー対象としています。
{
"source": ["aws.cloudwatch"],
"detail-type": ["CloudWatch Alarm State Change"],
"detail": {
"alarmName": ["Detect-AssumeRole-To-PowerRoles"],
"state": {
"value": ["ALARM"]
}
}
}
次に、ターゲットは以下のように指定しました。
ターゲットタイプ: AWS のサービス
ターゲットを選択:Step Functions ステートマシン
ステートマシン: (実行するステートマシン名をプルダウンから選択)
実行ロール: この特定のリソースについて新しいロールを作成
実行ロールは、この特定のリソースについて新しいロールを作成
から作成すると以下のようなロール・ポリシーが作成されます。
ロール・信頼関係
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "events.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"states:StartExecution"
],
"Resource": [
"${指定したStep Functions ステートマシンのARN}"
]
}
]
}
ありがちな、自動生成すると余計な権限がついてくる、ということはなく、最小権限でした。
Step Functions~Slack
Step Functionsステートマシンは以下の通りです。
実行ロール
新しいロールを作成
により、cloudwatch:GetMetricData
以外の権限については、Resourceの指定も含めた最小権限を達成できます。
グラフ
コード
{
"Comment": "A description of my state machine",
"StartAt": "GetMetricData",
"States": {
"GetMetricData": {
"Type": "Task",
"Arguments": {
"EndTime": "{% $states.context.Execution.StartTime %}",
"MetricDataQueries": [
{
"Expression": "SELECT MAX(AssumeRoleEventCount) FROM CloudTrailMetrics GROUP BY AccountId, RoleArn, principalId",
"Id": "q1",
"Period": 10,
"Label": "Detect-AssumeRole"
}
],
"StartTime": "{% $fromMillis($toMillis($states.context.Execution.StartTime) - 5*60*1000,'[Y0001]-[M01]-[D01]T[H01]:[m]:[s][Z]') %}"
},
"Resource": "arn:aws:states:::aws-sdk:cloudwatch:getMetricData",
"Next": "Call HTTPS APIs"
},
"Call HTTPS APIs": {
"Type": "Task",
"Resource": "arn:aws:states:::http:invoke",
"Arguments": {
"ApiEndpoint": "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
"Method": "POST",
"Headers": {
"content-type": "application/json"
},
"Authentication": {
"ConnectionArn": "arn:aws:events:ap-northeast-1:123456781234:connection/MyStateMachine-GetMetricData-DummyConnection/858546a1-af18-4e63-9417-43cc7d940ce6"
},
"RequestBody": {
"text": "{% $join($states.input.MetricDataResults.Label, '\\n') %}"
}
},
"Retry": [
{
"ErrorEquals": [
"States.ALL"
],
"BackoffRate": 2,
"IntervalSeconds": 1,
"MaxAttempts": 3,
"JitterStrategy": "FULL"
}
],
"End": true
}
},
"QueryLanguage": "JSONata"
}
GetMetricData
GetMetricData
タスクでのポイントは、
-
StartTime
とEndTime
の取り扱い -
MetricDataQueries
のExpression
です。
まず、StartTime
とEndTime
の取り扱いについてです。
このStep Functionsステートマシンは、CloudWatch AlarmがALARM状態に変化したことをトリガーに発火するものです。
つまり、確認するメトリクスの範囲は、EndTime
がステートマシンの実行タイミングと同時、StartTime
をステートマシンの実行タイミングの数分前(今回は5分前までとします)とすればよい、ということです。
実行タイミングはContextオブジェクトから取得できます。
"EndTime": "{% $states.context.Execution.StartTime %}"
とすることで、EndTime
をステートマシンの実行タイミングと同時としています。
次に、StartTime
です。Step Functionsで時刻を扱う上で取りうる方法として、JSONataの関数が使用できます。
そのために、QueryLanguage
フィールドでJSONata
を指定しています。
Step FunctionsでのJSONataのサポートは、変数と同時に2024/11/22より利用可能となった新しい機能で、豊富な組み込み関数による柔軟なデータ変換、というメリットを享受できるようになりました。
"StartTime": "{% $fromMillis($toMillis($states.context.Execution.StartTime) - 5*60*1000,'[Y0001]-[M01]-[D01]T[H01]:[m]:[s][Z]') %}"
として、「ステートマシンの実行タイミングの5分前」を取得しています。
toMillis
でISO8601拡張形式をエポックミリ秒に変換の上、5分前(5*60*1000ミリ秒前
)を計算し、fromMillis
でISO8601拡張形式に戻しています。
次に、MetricDataQueries
のExpression
についてです。
MetricDataQueryでは、Expression
としてMetrics Insights queryを指定します。
今回の例では、
"Expression": "SELECT MAX(AssumeRoleEventCount) FROM CloudTrailMetrics GROUP BY AccountId, RoleArn, principalId"
としており、CloudTrailログのCloudWatch ロググループに設定した、特定の複数の強権限ロールへのAssumeRoleをフィルターパターンとしたメトリクスフィルターで設定したディメンション、AccountId
、RoleArn
、principalId
をGROUP BY
句に含めることができています。
Call HTTPS APIs
Call HTTPS APIs
タスクでは、
-
Authentication
の設定 - 入力を基にした
RequestBody
の組み立て
がポイントです。
まず、Authentication
の設定についてです。
そもそも、本記事をお読みの方の多くはご存じであろうと思いますが、SlackのIncoming Webhookでは認証が不要です。
しかし、Call HTTPS APIs
タスクでは、APIの認証情報を安全に管理するためのEventBridge 接続リソースの指定が必須となっており、Authentication
もしくはInvocationConfig
フィールドのConnectionArn
として、EventBridge接続の接続 ARNを指定する必要があります。
これは省略できないものとなっているため、適当な値を設定したEventBridge接続を作成することとなります。
Step Functions Workflow Studio上では、新しい接続を作成
よりEventBridge接続を作成します。
SlackのIncoming Webhookでは、APIタイプをパブリック
とし、認証タイプをAPIキー
で適当な値を指定、で正常に呼び出しが可能なことを確認しています。
次に、入力を基にしたRequestBody
の組み立てについてです。
GetMetricData
タスクの出力、すなわちCall HTTPS APIs
タスクの入力は以下のような形式になっています。
{
"Messages": [],
"MetricDataResults": [
{
"Id": "q1",
"Label": "Detect-AssumeRole 123456781234 arn:aws:iam::123456781234:role/AdminRole AROXXXXXXXXXXXXXXXXX:sato_taro@works.com",
"StatusCode": "Complete",
"Timestamps": [
"2024-12-08T22:33:00Z"
],
"Values": [
1
]
},
{
"Id": "q1",
"Label": "Detect-AssumeRole 123456781234 arn:aws:iam::123456781234:role/IAMPowerRole AROXXXXXXXXXXXXXXXXX:kato_jiro@works.com",
"StatusCode": "Complete",
"Timestamps": [
"2024-12-08T22:33:00Z"
],
"Values": [
1
]
}
]
}
AccountId
、RoleArn
、principalId
のグループ化条件が、MetricDataResults
の中のLabel
フィールドとなっていることが分かります。
すなわち、MetricDataResults
のLabelを抽出することで、過去5分間の間に、どのAWSアカウントのどのプリンシパルが、自AWSアカウントのどのIAMロールにAssumeRoleしたか、を把握することができる、ということです。
こうしたJSONの操作にも、JSONataの組み込み関数が活用できます。
RequestBody
は以下のように定義しています。
"RequestBody": {
"text": "{% $join($states.input.MetricDataResults.Label, '\\n') %}"
}
$states.input.MetricDataResults.Label
では、$states.input
で入力を呼び出しており、後ろの.MetricDataResults.Label
では、JSON配列に対するJSONataによる操作により、Label
の値すべてを一つの配列として返しています。
その配列を、関数joinにより、改行コードをセパレータに指定し、改行を含む一つの文字列として連結しています。
連結した文字列をSlackへIncoming Webhookで送ると、Slack側で以下のようにポストされることとなります。
以上で、CloudWatch AlarmのALARM状態をトリガーとし、ディメンションのどの対象が閾値を超えたか、をSlack上で知ることができるようになりました。
最後に
最初はCloudWatchダッシュボードと、Step FunctionsのGetMetricWidgetImage
タスクを使い、ダッシュボードの画像をSlackに連携することを考えていました。
が、SlackのIncoming Webhooksでは直接画像を取り扱う方法はなく、attachmentsのimage_url
に画像URLを指定するか、SlackへのFileUploadを行うWebAPI/SDKを扱う(注: 旧来のfiles.upload APIは2025/3で廃止 )かのどちらかが必要になります。
前者は、パブリックにアクセスできる画像URLを用意する必要があり、ファイルアップロードから削除までのライフサイクルを考えたり、そもそもメトリクスを示す画像をパブリックにアクセスできるURLに置くことのセキュリティ上の懸念などを考えると難しいところがあります。
後者は、SDKを使う場合はLambdaの利用が避けられず、APIを直接叩く場合も、引用記事にある流れをStep FunctionsのStatesの複雑な組み合わせで実現するか、Lambdaを利用するか、になることと、Incoming Webhookだけでの実現と異なり、Slackのアクセストークンを扱う必要が出てくることから、手軽さに欠けることになります。
そのため方針転換をし、今回の形に落ち着きました。
ただ、メトリクスグラフをそのままSlackで見られる形の方が使い勝手がいいシチュエーションもあるかと思います。
ダッシュボードの画像をSlackに連携するいい方法が思いついたら、また別記事を投稿しようと思います。