6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「Develop fun!」を体現する! Works Human IntelligenceAdvent Calendar 2024

Day 8

CloudWatch Alarm発報時に、メトリクスのディメンションをSlackに通知する

Last updated at Posted at 2024-12-08

はじめに

CloudWatchメトリクスに対しAlarmを仕掛け、SNS、AWS Chatbotを介してSlackへ、メトリクスの状況変化の発報を行うという構成は、皆さんお使いかと思います。
ただ、Alarmの特性上、ディメンションを指定したメトリクスを対象にすることができないため、この形での発報だけでは、例えばどのEC2インスタンスのCPU利用率が閾値を超えたか、といった、ディメンションのどの対象が閾値を超えたか、をSlack上から知ることはできない、ということになります。

そこで、このディメンションの情報のSlackへの連携を、保守コストの高いアプリケーションロジックの実装(Lambdaなど)なしに実現する、をゴールとし、本記事をお届けします。

構成

最終的な構成は以下のようになりました。
名称未設定ファイル (1).png

  • EventBridgeルールで、対象のCloudWatch Alarmに対し、CloudWatch Alarm State Changeで、ALARM状態への変更時にStep Functionsステートマシンを呼び出す
  • Step Functionsステートマシンで、GetMetricDataタスクで目的のメトリクス値を取得し、Call HTTPS APIsタスクでSlackのIncoming Webhookへ送信

ということをやっています。

EventBridge

EventBridgeでは、イベントパターンとターゲットを指定していきます。イベントバスは、こだわりが無ければデフォルトイベントバスでよいかと思います。

イベントパターンは、今回は以下のようにいたしました。
sourcedetail-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ステートマシンは以下の通りです。

実行ロール

image.png
新しいロールを作成により、cloudwatch:GetMetricData以外の権限については、Resourceの指定も含めた最小権限を達成できます。

グラフ

stepfunctions_graph.png

コード

{
  "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タスクでのポイントは、

  • StartTimeEndTimeの取り扱い
  • MetricDataQueriesExpression
    です。

まず、StartTimeEndTimeの取り扱いについてです。

この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拡張形式に戻しています。

次に、MetricDataQueriesExpressionについてです。
MetricDataQueryでは、ExpressionとしてMetrics Insights queryを指定します。
今回の例では、
"Expression": "SELECT MAX(AssumeRoleEventCount) FROM CloudTrailMetrics GROUP BY AccountId, RoleArn, principalId"
としており、CloudTrailログのCloudWatch ロググループに設定した、特定の複数の強権限ロールへのAssumeRoleをフィルターパターンとしたメトリクスフィルターで設定したディメンション、AccountIdRoleArnprincipalIdGROUP 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接続を作成します。
スクリーンショット 2024-12-09 044449.png
SlackのIncoming Webhookでは、APIタイプをパブリックとし、認証タイプをAPIキーで適当な値を指定、で正常に呼び出しが可能なことを確認しています。
スクリーンショット 2024-12-09 044658.png

次に、入力を基にした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
      ]
    }
  ]
}

AccountIdRoleArnprincipalIdのグループ化条件が、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側で以下のようにポストされることとなります。
スクリーンショット 2024-12-09 060744.png

以上で、CloudWatch AlarmのALARM状態をトリガーとし、ディメンションのどの対象が閾値を超えたか、をSlack上で知ることができるようになりました。

最後に

最初はCloudWatchダッシュボードと、Step FunctionsのGetMetricWidgetImageタスクを使い、ダッシュボードの画像をSlackに連携することを考えていました。
が、SlackのIncoming Webhooksでは直接画像を取り扱う方法はなく、attachmentsimage_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に連携するいい方法が思いついたら、また別記事を投稿しようと思います。

6
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?