CloudWatchからSlackへの監視通知環境を簡単に構築する

CloudWatchの監視結果を簡単かつ汎用的にSlackへ通知する環境を用意したく、以下の構成で作成しました。   
CloudWatchアラーム -> SNSトピック -> Lambda -> Slack   

## 1. Slack側の設定
監視通知先のSlackチャンネルと[Incoming Webhooks](https://api.slack.com/messaging/webhooks)または[Webhooksワークフロー](https://slack.com/intl/ja-jp/help/articles/360041352714-%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B---Slack-%E5%A4%96%E9%83%A8%E3%81%A7%E9%96%8B%E5%A7%8B%E3%81%95%E3%82%8C%E3%82%8B%E3%83%AF%E3%83%BC%E3%82%AF%E3%83%95%E3%83%AD%E3%83%BC%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)を用意し、外部から叩くためのwebhookのURLを発行します。
また、受け取った通知メッセージをチャンネルに表示するためのパラメータ（今回はパラメータ名を`message`としている）設定を行います。
（作成方法など詳しくは各リンク先のドキュメント参照）

## 2. SNS (Simple Notification Service)でトピックの作成
* `トピックの作成`
    * タイプ : `スタンダード`

## 3. Lambda関数の作成
* `関数の作成`
    * トリガー : 上記で作成したSNSトピックを選択
    * 環境変数 : `SLACK_WEBHOOK_URL` : 発行したwebhookのURL
    * コード : e.g.
```py
import json
import os
import urllib3

# Slack Webhook URL
SLACK_WEBHOOK_URL = os.getenv("SLACK_WEBHOOK_URL")


def lambda_handler(event, context):
    http = urllib3.PoolManager()

    # SNSからのメッセージを取得
    sns_message = event["Records"][0]["Sns"]["Message"]
    # jsonのパース
    data = json.loads(sns_message)

    # Slackに送るメッセージを作成
    message = f"- **Alarm Name** :  {data['AlarmName']} \n"
    message += f"- **Description** :  {data['AlarmDescription']} \n"
    message += f"- **Reason** : {data['NewStateReason']} \n"
    message += f"- **New State** : {data['NewStateValue']} \n"
    message += f"- **Old State** : {data['OldStateValue']} \n"
    
    slack_message = {
        "message": f"{message}"
    }

    # Slackにメッセージを送信
    response = http.request(
        "POST",
        SLACK_WEBHOOK_URL,
        body=json.dumps(slack_message),
        headers={"Content-Type": "application/json"}
    )

    return {
        "statusCode": response.status,
        "body": response.data
    }
```

## 4. CloudWatchアラームの作成
1. CloudWatchコンソールから`アラームの作成`
    1. メトリクスと条件の指定 : 監視したいメトリクスと通知する際の閾値などの条件を決めます
    1. アクションの設定 : 
        1. アラーム状態トリガー : `アラーム状態`
        1. 次のSNSトピックに通知を送信 : `既存の SNS トピックを選択`
        1. 通知の送信先 : 先ほど作成したSNSトピックを選択
    1. 名前と説明
        1. アラーム名 : Lambdaで`AlarmName`として受け取る値
        1. アラームの説明 : Lambdaで`AlarmDescription`として受け取る値


これでアラームを作成すると、メトリクス条件に合致した場合にSlackへ通知が飛ぶようになります。
アラームを追加したい場合も同じSNSトピックを使い回しながら同じように作成していくことが可能です。
SNSからLambdaへ通知されるメッセージのjsonは以下のようなフォーマットになっています。

```json
{
    "AlarmName": "sample-alarm",
    "AlarmDescription": "This is a sample alarm.",
    "AWSAccountId": "XXXXXXXXXXXX",
    "AlarmConfigurationUpdatedTimestamp": "2025-01-01T00:30:35.137+0000",
    "NewStateValue": "ALARM",
    "NewStateReason": "Threshold Crossed: 1 out of the last 1 datapoints [2.0 (01/01/25 01:35:45)] was greater than the threshold (1.0) (minimum 1 datapoint for OK -> ALARM transition).",
    "StateChangeTime": "2025-01-01T01:35:45.133+0000",
    "Region": "US East (N. Virginia)",
    "AlarmArn": "arn:aws:cloudwatch:us-east-1:XXXXXXXXXXXX:alarm:sample-alarm",
    "OldStateValue": "OK",
    "OKActions": [],
    "AlarmActions": [
        "arn:aws:sns:us-east-1:XXXXXXXXXXXX:sample-sns-topic"
    ],
    "InsufficientDataActions": [],
    "Trigger": {
        "MetricName": "RequestCount",
        "Namespace": "AWS/ApplicationELB",
        "StatisticType": "Statistic",
        "Statistic": "SUM",
        "Unit": null,
        "Dimensions": [
            {
                "value": "app/sample-elb/XXXXXXXXXXXX",
                "name": "LoadBalancer"
            }
        ],
        "Period": 60,
        "EvaluationPeriods": 1,
        "DatapointsToAlarm": 1,
        "ComparisonOperator": "GreaterThanThreshold",
        "Threshold": 1.0,
        "TreatMissingData": "missing",
        "EvaluateLowSampleCountPercentile": ""
    }
}
```